From 98edc7c542e999a1af857d43e4e271828b4d6faa Mon Sep 17 00:00:00 2001 From: SpaceVoyage Date: Wed, 3 Jul 2024 17:29:09 -0500 Subject: [PATCH] init --- .dockerignore | 35 + .env.example | 22 + .github/ISSUE_TEMPLATE/bug_report.md | 38 + .github/ISSUE_TEMPLATE/feature_request.md | 20 + .gitignore | 156 + CONTRIBUTING.md | 20 + Dockerfile | 41 + LICENSE.md | 182 ++ README.md | 49 + compose.yaml | 21 + data/database.json | 1 + package-lock.json | 3582 +++++++++++++++++++++ package.json | 42 + src/.editorconfig | 5 + src/LICENSE | 1 + src/README.md | 14 + src/config/config.js | 1212 +++++++ src/connections/cameras.js | 422 +++ src/connections/courier.js | 305 ++ src/connections/database.js | 57 + src/connections/obs.js | 1286 ++++++++ src/connections/obsbot.js | 360 +++ src/connections/twitch.js | 295 ++ src/connections/unifi.js | 358 ++ src/controller.js | 44 + src/index.js | 31 + src/modules/legacy.js | 2251 +++++++++++++ src/utils/helper.js | 103 + src/utils/logger.js | 54 + src/utils/utilsModule.js | 236 ++ 30 files changed, 11243 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 compose.yaml create mode 100644 data/database.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/.editorconfig create mode 100644 src/LICENSE create mode 100644 src/README.md create mode 100644 src/config/config.js create mode 100644 src/connections/cameras.js create mode 100644 src/connections/courier.js create mode 100644 src/connections/database.js create mode 100644 src/connections/obs.js create mode 100644 src/connections/obsbot.js create mode 100644 src/connections/twitch.js create mode 100644 src/connections/unifi.js create mode 100644 src/controller.js create mode 100644 src/index.js create mode 100644 src/modules/legacy.js create mode 100644 src/utils/helper.js create mode 100644 src/utils/logger.js create mode 100644 src/utils/utilsModule.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9078736 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.next +**/.cache +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +**/build +**/dist +.editorconfig +LICENSE +README.md \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..61f705d --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +TWITCH_CLIENT_ID=xxxxxxxxxxxxx +TWITCH_CLIENT_SECRET=xxxxxxxxxx +TWITCH_TOKEN_PATH=/tokens/tokens.json + +OBS_WS="ws://127.0.0.1:12345" +OBS_KEY="xxxxxxx" + +OBSBOT_HOST="127.0.0.1" +OBSBOT_PORT=12345 + +COURIER_KEY="xxxxxxxxx" + +AXIS_USERNAME=xxxxxx +AXIS_PASSWORD=xxxxxxxx + +AXIS_CAMERA_IP="127.0.0.1" + + +UNIFI_AP_MACS="{"ee:ee:ee:ee:ee:ee":"test"}" +UNIFI_IP="127.0.0.1" +UNIFI_USERNAME='xxxx' +UNIFI_PASSWORD='xxxxxx' \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..68fc05c --- /dev/null +++ b/.gitignore @@ -0,0 +1,156 @@ +data/database_* +tokens +src/data +.env* +!.env.example +*.bak + +# IDEs +.idea/ +.vscode/ + +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-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/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# 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 + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# 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 + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bdf5d7e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,20 @@ +# Contributing to Alveus.gg + +Hey there! Welcome to Alveus.gg! Here's some things to keep in mind before you get started. + +## Questions + +If you're looking to work on an issue but have a question about it or are generally unsure about how to approach it, reach out in that specific issue and folks will be able to help. + +For general questions about the project unrelated to a specific issue, please start a [discussion](https://github.com/orgs/alveusgg/discussions) (or file an issue if you believe what you're asking is something actionable in code). + +## Development + +### Dev Environment + +- To start a local dev environment, follow this [guide](https://github.com/alveusgg/chatbot#development-setup). + +### Commit Messages + +- Try to keep your commits to small units of change. Prefering more commits over less. +- Please keep your commit message to a concise message that clearly explains the change made. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..065e68a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/go/dockerfile-reference/ + +# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 + +ARG NODE_VERSION=20.13.1 + +FROM node:${NODE_VERSION}-slim + +ENV NPM_CONFIG_PREFIX=/home/node/.npm-global + +ENV PATH=$PATH:/home/node/.npm-global/bin + +# Use production node environment by default. +ENV NODE_ENV production + +WORKDIR /home/node/app + +# Download dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /root/.npm to speed up subsequent builds. +# Leverage a bind mounts to package.json and package-lock.json to avoid having to copy them into +# into this layer. +RUN --mount=type=bind,source=package.json,target=package.json \ + --mount=type=bind,source=package-lock.json,target=package-lock.json \ + --mount=type=cache,target=/root/.npm \ + npm ci --omit=dev + +# Run the application as a non-root user. +USER node + +# Copy the rest of the source files into the image. +COPY --chown=node:node . . + +# Expose the port that the application listens on. +#EXPOSE 3000 + +# Run the application. +CMD node src/index.js diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..3c9d8ac --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,182 @@ +# Alveus.gg License + +All code within this Alveus.gg project is released under the Apache License, +Version 2.0. + +Copyright 2023 Alveus Sanctuary Inc. and the Alveus.gg team + + +This license does not apply to any images or other media contained within the +project. These remain the property of their respective owners. Please contact +the Alveus team if you wish to license any images or other media for use in your +own projects, or if you have any questions about the license. + +## Apache License + +_Version 2.0, January 2004_ +_<>_ + +### 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_ diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa3c0a6 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Alveus Sanctuary Chat Bot + +This is the Twitch Chat bot for Alveus Sanctuary, allowing stream viewers to control the tech running the Livecams. +You can access the stream at [twitch.tv/alveussanctuary](https://www.twitch.tv/alveussanctuary). + +## See also +- [Website repository](https://github.com/alveusgg/alveusgg) +- [Data repository](https://github.com/alveusgg/data) +- [Twitch extension](https://github.com/alveusgg/extension) + +## Tech stack + +This project uses Docker to run a Node.js app. + +For development: + +- Node.js +- Prettier (code formatting) +- ESLint (code linting) +- Docker (Compose) + +## External APIs + +- Twurple Twitch API Library +- Courier Notification Platform + +## How to contribute + +Hey there! Welcome to Alveus.gg! There's a few ways that you can help contribute. + +1. If you find a bug - you can fill out a bug [report](https://github.com/alveusgg/chatbot/issues/new/choose) +2. If you have an idea that would make Alveus better - please fill out an idea [issue](https://github.com/alveusgg/chatbot/issues/new/choose) +3. If you have development experience, take a look at our issues labeled [good first issue](https://github.com/alveusgg/alveusgg/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22), read our [contributing guide](https://github.com/alveusgg/chatbot/blob/main/CONTRIBUTING.md) and agree to our [code of conduct](https://github.com/alveusgg/.github/blob/main/CODE_OF_CONDUCT.md) before you get started. + +## Development setup + +### Local development + +1. Install Node.js (see `engines` in `package.json` for the required versions). +2. Copy `.env.example` to `.env` and open your copy in a text editor and fill it: + 1. Twitch Client Id, Secret, and Token Path. + 2. OBS Websocket Address and Key. + 3. Axis Camera Username, Password, and IP Addresses. + 4. Courier Key. + 5. OBSBot Address and Port. + 6. Unifi Address, Username, Password, and a map of Mac addresses to Names. +3. Add Twitch API tokens to `tokens` folder. +4. Configure the settings `src/config/config.js`. +5. Run `docker compose up -d`. \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..b440a42 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,21 @@ +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Docker Compose reference guide at +# https://docs.docker.com/go/compose-spec-reference/ + +# Here the instructions define your application as a service called "server". +# This service is built from the Dockerfile in the current directory. +# You can add other services your application may depend on here, such as a +# database or a cache. For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose +services: + alveuscontroller: + container_name: alveuscontroller + build: + context: . + environment: + NODE_ENV: production + restart: always + volumes: + - /opt/docker/alveuscontroller/data:/home/node/app/src/data + - /opt/docker/alveuscontroller/tokens:/home/node/app/src/tokens/ + - /opt/docker/alveuscontroller/.env:/home/node/app/src/.env diff --git a/data/database.json b/data/database.json new file mode 100644 index 0000000..b4f7054 --- /dev/null +++ b/data/database.json @@ -0,0 +1 @@ +{"pasture":{"presets":{"insidebarn":{"pan":38.39,"tilt":-6.02,"zoom":1,"focus":7231,"brightness":5000,"autofocus":"on","autoiris":"on"},"trailer":{"pan":-24.48,"tilt":-4,"zoom":161,"focus":7678,"brightness":5000,"autofocus":"on","autoiris":"on"},"grove":{"pan":-56.95,"tilt":-4.01,"zoom":983,"focus":7121,"brightness":5000,"autofocus":"on","autoiris":"on"},"trailerhay":{"pan":-26.67,"tilt":-4,"zoom":909,"focus":7097,"brightness":5000,"autofocus":"on","autoiris":"on"},"feedstall":{"pan":"48.42","tilt":"-36.72","zoom":"1","focus":"4383","brightness":"5100","autofocus":"on","autoiris":"on"},"home":{"pan":"-47.63","tilt":"-7.54","zoom":"1","focus":"7142","brightness":"5100","autofocus":"on","autoiris":"on"},"pool":{"pan":"1.56","tilt":"-0.69","zoom":"1001","focus":"6629","brightness":"5000","autofocus":"on","autoiris":"on"},"sky":{"pan":-41,"tilt":7.4,"zoom":1,"focus":2003,"brightness":5000,"autofocus":"on","autoiris":"on"},"right":{"pan":14.94,"tilt":0.02,"zoom":1114,"focus":7231,"brightness":5000,"autofocus":"on","autoiris":"on"},"pole":{"pan":-15.39,"tilt":-0.49,"zoom":808,"focus":6882,"brightness":5000,"autofocus":"on","autoiris":"on"},"feeder":{"pan":-58.49,"tilt":-31.42,"zoom":1,"focus":5573,"brightness":5000,"autofocus":"on","autoiris":"on"},"brush":{"pan":"-48.37","tilt":"-6.20","zoom":"683","focus":"7321","brightness":"5000","autofocus":"on","autoiris":"on"},"poolr":{"pan":11.72,"tilt":0,"zoom":914,"focus":7321,"brightness":5000,"autofocus":"on","autoiris":"on"},"pooll":{"pan":-8.61,"tilt":-1.99,"zoom":1157,"focus":7499,"brightness":5000,"autofocus":"on","autoiris":"on"},"barn":{"pan":-6.7,"tilt":-6.6,"zoom":1,"focus":5573,"brightness":5000,"autofocus":"on","autoiris":"on"},"barn2":{"pan":-67.82,"tilt":-5.06,"zoom":1594,"focus":7002,"brightness":5000,"autofocus":"on","autoiris":"on"},"poolpole":{"pan":-2.51,"tilt":-0.75,"zoom":1008,"focus":7499,"brightness":5000,"autofocus":"on","autoiris":"on"},"sandpit":{"pan":-37.1,"tilt":-4.44,"zoom":1021,"focus":7410,"brightness":5000,"autofocus":"on","autoiris":"on"},"purplebase":{"pan":-42.43,"tilt":-3.58,"zoom":1198,"focus":7321,"brightness":5000,"autofocus":"on","autoiris":"on"},"purplenest":{"pan":-42.75,"tilt":-0.89,"zoom":7720,"focus":6817,"brightness":5000,"autofocus":"on","autoiris":"on"},"purplenestw":{"pan":-42.77,"tilt":-0.38,"zoom":5218,"focus":6905,"brightness":5000,"autofocus":"on","autoiris":"on"},"winnie":{"pan":18.25,"tilt":-2.1,"zoom":4914,"focus":6665,"brightness":5000,"autofocus":"on","autoiris":"on"},"fencenear":{"pan":-67.87,"tilt":-13.11,"zoom":399,"focus":6209,"brightness":5000,"autofocus":"on","autoiris":"on"},"fencefar":{"pan":-68.92,"tilt":-6,"zoom":750,"focus":7410,"brightness":5000,"autofocus":"on","autoiris":"on"},"angel":{"pan":-72.09,"tilt":-47.43,"zoom":1,"focus":3192,"brightness":5000,"autofocus":"on","autoiris":"on"},"barn2w":{"pan":-64.91,"tilt":-5.05,"zoom":796,"focus":7678,"brightness":5000,"autofocus":"on","autoiris":"on"},"feederinspect":{"pan":131.29,"tilt":19.99,"zoom":300,"focus":812,"brightness":5000,"autofocus":"on","autoiris":"on"},"bughunt":{"pan":58.38,"tilt":-51.58,"zoom":1,"focus":5573,"brightness":5000,"autofocus":"on","autoiris":"on"},"barn2r":{"pan":-63.56,"tilt":-4,"zoom":1296,"focus":7321,"brightness":5000,"autofocus":"on","autoiris":"on"},"feederinspect2":{"pan":-88.47,"tilt":-40.88,"zoom":2001,"focus":4118,"brightness":5000,"autofocus":"on","autoiris":"on"},"zoomie1":{"pan":-45.48,"tilt":-0.53,"zoom":157,"focus":7231,"brightness":5000,"autofocus":"on","autoiris":"on"},"zoomie2":{"pan":2.06,"tilt":1.34,"zoom":199,"focus":7499,"brightness":5000,"autofocus":"on","autoiris":"on"},"barn2hay":{"pan":-65.47,"tilt":-5.34,"zoom":2094,"focus":6989,"brightness":5000,"autofocus":"on","autoiris":"on"},"trailerl":{"pan":-34.49,"tilt":-3.04,"zoom":660,"focus":7499,"brightness":5000,"autofocus":"on","autoiris":"on"},"feederinspect3":{"pan":-87.49,"tilt":-36.44,"zoom":500,"focus":5313,"brightness":5000,"autofocus":"on","autoiris":"on"},"sunrise":{"pan":-47.64,"tilt":2.44,"zoom":1,"focus":7231,"brightness":5000,"autofocus":"on","autoiris":"on"},"water":{"pan":18.3,"tilt":-1.37,"zoom":700,"focus":7410,"brightness":5000,"autofocus":"on","autoiris":"on"},"roundpen":{"pan":-20.16,"tilt":-1.99,"zoom":554,"focus":7499,"brightness":5000,"autofocus":"on","autoiris":"on"},"waterclose":{"pan":18.86,"tilt":-2.86,"zoom":3201,"focus":6682,"brightness":5000,"autofocus":"on","autoiris":"on"},"bugbox":{"pan":81.75,"tilt":-50.69,"zoom":1299,"focus":3281,"brightness":5000,"autofocus":"on","autoiris":"on"},"bbur":{"pan":86.77,"tilt":-48.83,"zoom":2798,"focus":3490,"brightness":5000,"autofocus":"on","autoiris":"on"},"penl":{"pan":-27.44,"tilt":-3.05,"zoom":1108,"focus":7410,"brightness":5000,"autofocus":"on","autoiris":"on"},"penr":{"pan":-14.4,"tilt":-2.51,"zoom":1108,"focus":7499,"brightness":5000,"autofocus":"on","autoiris":"on"},"sanddonk":{"pan":-25.75,"tilt":-3.61,"zoom":2702,"focus":7002,"brightness":5000,"autofocus":"on","autoiris":"on"},"pen":{"pan":-20.51,"tilt":-2.36,"zoom":624,"focus":7321,"brightness":5000,"autofocus":"on","autoiris":"on"},"fencemiddle":{"pan":-67.83,"tilt":-9.91,"zoom":697,"focus":7231,"brightness":5000,"autofocus":"on","autoiris":"on"},"tmpwinnie":{"pan":11.9,"tilt":-6.21,"zoom":798,"focus":6882,"brightness":5000,"autofocus":"on","autoiris":"on"},"temp":{"pan":-45.64,"tilt":-3.62,"zoom":9906,"focus":6809,"brightness":5000,"autofocus":"on","autoiris":"on"},"winnietmp":{"pan":44.87,"tilt":-6.91,"zoom":700,"focus":6336,"brightness":5000,"autofocus":"on","autoiris":"on"},"jalapenotmp":{"pan":-24.89,"tilt":-3.93,"zoom":3107,"focus":6907,"brightness":5000,"autofocus":"on","autoiris":"on"},"donktmp":{"pan":-31.44,"tilt":-3.32,"zoom":4107,"focus":6875,"brightness":5000,"autofocus":"on","autoiris":"on"},"brushr":{"pan":-43.89,"tilt":-6.2,"zoom":1181,"focus":6971,"brightness":5000,"autofocus":"on","autoiris":"on"},"penm":{"pan":-20.47,"tilt":-3.06,"zoom":1108,"focus":7589,"brightness":5000,"autofocus":"on","autoiris":"on"},"picnic":{"pan":53.72,"tilt":-36.77,"zoom":1001,"focus":5076,"brightness":5000,"autofocus":"on","autoiris":"on"},"stompyfood":{"pan":53.97,"tilt":-8,"zoom":399,"focus":6209,"brightness":5000,"autofocus":"on","autoiris":"on"},"stompytmp":{"pan":-28.18,"tilt":-3.42,"zoom":5108,"focus":6829,"brightness":5000,"autofocus":"on","autoiris":"on"},"saltlickz":{"pan":6.45,"tilt":0.01,"zoom":7003,"focus":6573,"brightness":5000,"autofocus":"off","autoiris":"on"},"waspnest":{"pan":18.53,"tilt":15.87,"zoom":4028,"focus":4922,"brightness":5000,"autofocus":"on","autoiris":"on"},"waterz":{"pan":18.79,"tilt":-2.85,"zoom":2700,"focus":6645,"brightness":5000,"autofocus":"on","autoiris":"on"},"dtemp":{"pan":-45.34,"tilt":-3.49,"zoom":10909,"focus":6789,"brightness":5000,"autofocus":"off","autoiris":"on"},"donksleep":{"pan":-23.5,"tilt":-3.83,"zoom":2922,"focus":6832,"brightness":5000,"autofocus":"on","autoiris":"on"}},"isRoaming":false,"roamTime":"5","roamSpeed":"40","roamIndex":1,"roamDirection":"forward","roamList":["barn","pool","roundpen","trailerl","purplebase","grove","barn2w","home"],"speed":"60","lastKnownPosition":{"pan":"-57.56","tilt":"-7.52","zoom":"401","focus":"6882","brightness":"5100","autofocus":"on","autoiris":"on"}},"parrot":{"presets":{"home":{"pan":37.98,"tilt":-11.4,"zoom":1,"focus":6006,"brightness":5000,"autofocus":"on","autoiris":"on"},"door":{"pan":58.81,"tilt":-12,"zoom":1,"focus":5005,"brightness":5000,"autofocus":"on","autoiris":"on"},"abovedoor":{"pan":65.31,"tilt":-1,"zoom":446,"focus":5005,"brightness":5000,"autofocus":"on","autoiris":"on"},"littles":{"pan":72.31,"tilt":-9.99,"zoom":80,"focus":3343,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlesw":{"pan":42.27,"tilt":-9,"zoom":1062,"focus":6374,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlest":{"pan":76.71,"tilt":0,"zoom":667,"focus":5755,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlesbowl":{"pan":70.29,"tilt":-13.99,"zoom":766,"focus":6006,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlesplatform":{"pan":41.53,"tilt":-18,"zoom":970,"focus":6283,"brightness":5000,"autofocus":"on","autoiris":"on"},"littles2":{"pan":50.26,"tilt":-4.99,"zoom":462,"focus":5207,"brightness":5000,"autofocus":"on","autoiris":"on"},"littles2top":{"pan":54,"tilt":0,"zoom":715,"focus":5673,"brightness":5000,"autofocus":"on","autoiris":"on"},"littles2bottom":{"pan":51,"tilt":-7.99,"zoom":1015,"focus":6741,"brightness":5000,"autofocus":"on","autoiris":"on"},"macaws":{"pan":27.15,"tilt":-15,"zoom":1,"focus":3754,"brightness":5000,"autofocus":"on","autoiris":"on"},"macawsw":{"pan":10.2,"tilt":-10.73,"zoom":1074,"focus":6374,"brightness":5000,"autofocus":"on","autoiris":"on"},"macawst":{"pan":19.59,"tilt":-3.99,"zoom":264,"focus":3089,"brightness":5000,"autofocus":"on","autoiris":"on"},"macawsb":{"pan":28.66,"tilt":-26.83,"zoom":201,"focus":1,"brightness":5000,"autofocus":"on","autoiris":"on"},"macawsbowl":{"pan":20.82,"tilt":-15,"zoom":833,"focus":5642,"brightness":5000,"autofocus":"on","autoiris":"on"},"macawsplatform":{"pan":65.86,"tilt":-30,"zoom":485,"focus":5607,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlessleep":{"pan":55.38,"tilt":-1.45,"zoom":1715,"focus":6119,"brightness":5000,"autofocus":"on","autoiris":"on"},"bothwindows":{"pan":26.19,"tilt":-9.99,"zoom":74,"focus":3343,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlestablet":{"pan":39,"tilt":-9,"zoom":914,"focus":6315,"brightness":5000,"autofocus":"on","autoiris":"on"},"macaws2":{"pan":29.15,"tilt":-18.99,"zoom":1,"focus":1252,"brightness":5000,"autofocus":"on","autoiris":"on"},"macaws2bowl":{"pan":35.65,"tilt":-21,"zoom":249,"focus":3754,"brightness":5000,"autofocus":"on","autoiris":"on"},"clock":{"pan":23.2,"tilt":0,"zoom":1035,"focus":6381,"brightness":5000,"autofocus":"on","autoiris":"on"},"tablet":{"pan":36.94,"tilt":-9.99,"zoom":1800,"focus":6905,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlesb":{"pan":73.29,"tilt":-16.99,"zoom":566,"focus":5150,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlesbowl2":{"pan":78.3,"tilt":-16.99,"zoom":864,"focus":5755,"brightness":5000,"autofocus":"on","autoiris":"on"},"parrotsw":{"pan":33.75,"tilt":-12.99,"zoom":1,"focus":5005,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlesoutside":{"pan":63.81,"tilt":-7.99,"zoom":100,"focus":2865,"brightness":5000,"autofocus":"on","autoiris":"on"},"floor":{"pan":47.49,"tilt":-30,"zoom":1,"focus":5005,"brightness":5000,"autofocus":"on","autoiris":"on"},"chickensl":{"pan":53.82,"tilt":-18.14,"zoom":715,"focus":6452,"brightness":5000,"autofocus":"on","autoiris":"on"},"chickensr":{"pan":84.81,"tilt":-19.99,"zoom":599,"focus":5720,"brightness":5000,"autofocus":"on","autoiris":"on"},"platform":{"pan":42.7,"tilt":-15.99,"zoom":568,"focus":5720,"brightness":5000,"autofocus":"on","autoiris":"on"},"training":{"pan":57.24,"tilt":-18,"zoom":599,"focus":5863,"brightness":5000,"autofocus":"on","autoiris":"on"},"siren":{"pan":55.37,"tilt":-6,"zoom":1661,"focus":6655,"brightness":5000,"autofocus":"on","autoiris":"on"},"tico":{"pan":12.25,"tilt":-10.99,"zoom":774,"focus":5005,"brightness":5000,"autofocus":"on","autoiris":"on"},"littles2middle":{"pan":50.76,"tilt":-4.99,"zoom":862,"focus":6506,"brightness":5000,"autofocus":"on","autoiris":"on"},"mouse":{"pan":58.65,"tilt":-19.99,"zoom":2001,"focus":7085,"brightness":5000,"autofocus":"on","autoiris":"on"},"mouser":{"pan":76.28,"tilt":-28.99,"zoom":500,"focus":5207,"brightness":5000,"autofocus":"on","autoiris":"on"},"mousel":{"pan":51.2,"tilt":-19.99,"zoom":715,"focus":6119,"brightness":5000,"autofocus":"on","autoiris":"on"},"mousec":{"pan":48.65,"tilt":-27,"zoom":500,"focus":5406,"brightness":5000,"autofocus":"on","autoiris":"on"},"floorr":{"pan":70.98,"tilt":-29.05,"zoom":1,"focus":1,"brightness":5000,"autofocus":"on","autoiris":"on"},"floorl":{"pan":20.59,"tilt":-39,"zoom":1,"focus":1,"brightness":5000,"autofocus":"on","autoiris":"on"},"floord":{"pan":47.49,"tilt":-50,"zoom":1,"focus":1,"brightness":5000,"autofocus":"on","autoiris":"on"},"backfloor":{"pan":27.15,"tilt":-22.99,"zoom":1,"focus":2503,"brightness":5000,"autofocus":"on","autoiris":"on"},"backcorner":{"pan":7.15,"tilt":-25.99,"zoom":498,"focus":5807,"brightness":5000,"autofocus":"on","autoiris":"on"},"chickensm":{"pan":58.56,"tilt":-16.99,"zoom":715,"focus":6452,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlesspot":{"pan":70.94,"tilt":-1.99,"zoom":1066,"focus":6311,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlesm":{"pan":75.31,"tilt":-9.99,"zoom":580,"focus":5150,"brightness":5000,"autofocus":"on","autoiris":"on"},"littles2bowl":{"pan":53.96,"tilt":-7.57,"zoom":1661,"focus":6713,"brightness":5000,"autofocus":"on","autoiris":"on"},"littlesleep":{"pan":56.52,"tilt":-1,"zoom":1012,"focus":6408,"brightness":5000,"autofocus":"on","autoiris":"on"},"temp":{"pan":43.87,"tilt":-9.2,"zoom":2619,"focus":6874,"brightness":5000,"autofocus":"on","autoiris":"on"},"tmp":{"pan":12.9,"tilt":-10.73,"zoom":574,"focus":3236,"brightness":5000,"autofocus":"on","autoiris":"on"},"mileytmp":{"pan":27,"tilt":-28.41,"zoom":1047,"focus":4409,"brightness":5000,"autofocus":"on","autoiris":"on"},"crownin":{"pan":28.66,"tilt":-33.01,"zoom":700,"focus":1956,"brightness":5000,"autofocus":"on","autoiris":"on"},"macawsbl":{"pan":26.07,"tilt":-28.44,"zoom":683,"focus":3637,"brightness":5000,"autofocus":"on","autoiris":"on"},"macawsbr":{"pan":41.99,"tilt":-21,"zoom":748,"focus":3383,"brightness":5000,"autofocus":"on","autoiris":"on"},"chickenslc":{"pan":51.95,"tilt":-17.39,"zoom":1215,"focus":6350,"brightness":5000,"autofocus":"on","autoiris":"on"},"leftscreen":{"pan":0.26,"tilt":0,"zoom":1,"focus":1,"brightness":5000,"autofocus":"on","autoiris":"on"},"sirentmp":{"pan":54.37,"tilt":-0.94,"zoom":3214,"focus":6542,"brightness":5000,"autofocus":"on","autoiris":"on"}},"isRoaming":false,"speed":"70","roamTime":"30","roamSpeed":"40","roamIndex":0,"roamDirection":"reverse","roamList":["tmp1","tmp2"]},"georgie":{"presets":{"water":{"pan":"4.27","tilt":"-55.01","zoom":"1","iris":"5000","focus":"6933","autofocus":"on"},"ramp":{"pan":"23.89","tilt":"-23.28","zoom":"3791","iris":"5000","focus":"5193","autofocus":"on"},"closedirt":{"pan":29.87,"tilt":-10.01,"zoom":10707,"focus":5510,"autofocus":"on"},"temp1":{"pan":28.21,"tilt":-14.88,"zoom":10909,"iris":5000,"focus":5279,"autofocus":"on"},"temp2":{"pan":28.21,"tilt":-14.88,"zoom":10509,"iris":5000,"focus":5279,"autofocus":"on"},"dirt":{"pan":29.37,"tilt":-9.29,"zoom":8360,"focus":5667,"autofocus":"on"},"wide":{"pan":28.51,"tilt":-14.21,"zoom":4002,"focus":6036,"autofocus":"on"},"shore":{"pan":22.42,"tilt":-22.64,"zoom":1387,"focus":6373,"autofocus":"on"},"closeup":{"pan":30.12,"tilt":-10.61,"zoom":10909,"focus":5510,"autofocus":"on"},"shoreright":{"pan":34.36,"tilt":-18.14,"zoom":9859,"focus":5062,"autofocus":"on"},"home":{"pan":33.31,"tilt":-22.12,"zoom":1,"focus":7466,"autofocus":"on"}},"isRoaming":false,"roamTime":"2","roamSpeed":"25","roamIndex":-1,"roamDirection":"forward","roamList":[]},"noodle":{"presets":{"hammoc":{"pan":"-104.87","tilt":"-20.43","zoom":"999","iris":"5000","focus":"6384","autofocus":"on"},"water":{"pan":"-117.67","tilt":"-53.92","zoom":"3401","iris":"5000","focus":"6339","autofocus":"on"},"box":{"pan":"-169.87","tilt":"-75.45","zoom":"1","iris":"5000","focus":"7086","autofocus":"on"}},"isRoaming":false},"crow":{"presets":{"platform":{"pan":"45.13","tilt":"-35.96","zoom":"2290","focus":"9037","brightness":"5000","autofocus":"on","autoiris":"on"},"window":{"pan":99.34,"tilt":-5.19,"zoom":500,"focus":9839,"brightness":5000,"autofocus":"on","autoiris":"on"},"water":{"pan":41.31,"tilt":-67.63,"zoom":1500,"focus":9450,"brightness":5000,"autofocus":"on","autoiris":"on"},"top":{"pan":"48.19","tilt":"-13.71","zoom":"1","focus":"9892","brightness":"5000","autofocus":"on","autoiris":"on"},"backcorner":{"pan":"45.77","tilt":"0.00","zoom":"2699","focus":"8829","brightness":"5000","autofocus":"on","autoiris":"on"},"outside":{"pan":"95.83","tilt":"-46.50","zoom":"1","focus":"9892","brightness":"5000","autofocus":"on","autoiris":"on"},"home":{"pan":"70.44","tilt":"-59.34","zoom":"1","focus":"9963","brightness":"5000","autofocus":"on","autoiris":"on"},"entry":{"pan":136.31,"tilt":-75.06,"zoom":1488,"focus":9404,"brightness":5000,"autofocus":"on","autoiris":"on"},"down":{"pan":71.34,"tilt":-75.89,"zoom":1,"focus":9998,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightperch":{"pan":113.27,"tilt":-10.09,"zoom":1,"focus":9733,"brightness":5000,"autofocus":"on","autoiris":"on"},"backleftcorner":{"pan":30.38,"tilt":-21.69,"zoom":4890,"focus":8636,"brightness":5000,"autofocus":"on","autoiris":"on"},"crowsleep":{"pan":45.77,"tilt":0,"zoom":2699,"focus":9107,"brightness":5000,"autofocus":"on","autoiris":"on"},"sleeptemp":{"pan":112.77,"tilt":-7.09,"zoom":4998,"focus":6763,"brightness":5000,"autofocus":"on","autoiris":"on"},"back":{"pan":46.34,"tilt":-45,"zoom":301,"focus":9928,"brightness":5000,"autofocus":"on","autoiris":"on"},"hose":{"pan":62.34,"tilt":-90,"zoom":1500,"focus":9574,"brightness":5000,"autofocus":"on","autoiris":"on"},"heater":{"pan":38.27,"tilt":0,"zoom":3698,"focus":8626,"brightness":5000,"autofocus":"on","autoiris":"on"},"table":{"pan":37.77,"tilt":-52,"zoom":2001,"focus":9339,"brightness":5000,"autofocus":"on","autoiris":"on"},"temp":{"pan":52.46,"tilt":0,"zoom":4697,"focus":8668,"brightness":5000,"autofocus":"on","autoiris":"on"},"dtemp":{"pan":44.27,"tilt":-32.13,"zoom":500,"focus":9857,"brightness":5000,"autofocus":"on","autoiris":"on"},"training":{"pan":46.34,"tilt":-35,"zoom":301,"focus":9928,"brightness":5000,"autofocus":"on","autoiris":"on"},"backcornerr":{"pan":51.02,"tilt":0,"zoom":5195,"focus":8583,"brightness":5000,"autofocus":"on","autoiris":"on"},"windowc":{"pan":96.34,"tilt":-8.18,"zoom":1500,"focus":8999,"brightness":5000,"autofocus":"on","autoiris":"on"},"down2":{"pan":71.34,"tilt":-84.95,"zoom":1,"focus":9998,"brightness":5000,"autofocus":"on","autoiris":"on"},"tmp":{"pan":28.86,"tilt":-55.33,"zoom":2001,"focus":9226,"brightness":5000,"autofocus":"on","autoiris":"on"}},"isRoaming":false,"speed":"100"},"crowoutdoor":{"presets":{"backcorner":{"pan":-150.2,"tilt":-1.51,"zoom":3496,"focus":8660,"brightness":5000,"autofocus":"on","autoiris":"on"},"platform":{"pan":-149.46,"tilt":-10.83,"zoom":3240,"focus":8797,"brightness":5000,"autofocus":"on","autoiris":"on"},"door":{"pan":"143.20","tilt":"-53.78","zoom":"1","focus":"9574","brightness":"5000","autofocus":"on","autoiris":"on"},"bench":{"pan":-106.05,"tilt":-10.86,"zoom":1000,"focus":9415,"brightness":5000,"autofocus":"on","autoiris":"on"},"water":{"pan":-128.91,"tilt":-31.15,"zoom":1600,"focus":8559,"brightness":5000,"autofocus":"on","autoiris":"on"},"home":{"pan":"-150.31","tilt":"-14.39","zoom":"1","focus":"9592","brightness":"5000","autofocus":"on","autoiris":"on"},"corner":{"pan":-105.85,"tilt":-4.86,"zoom":2500,"focus":8497,"brightness":5000,"autofocus":"on","autoiris":"on"},"left":{"pan":-158.31,"tilt":-6.88,"zoom":1,"focus":9433,"brightness":5000,"autofocus":"on","autoiris":"on"},"right":{"pan":"-125.31","tilt":"-6.39","zoom":"1","focus":"9592","brightness":"5000","autofocus":"on","autoiris":"on"},"ground":{"pan":-153.81,"tilt":-31.4,"zoom":1,"focus":9609,"brightness":5000,"autofocus":"on","autoiris":"on"},"treet":{"pan":"-176.46","tilt":"-1.00","zoom":"2500","focus":"8465","brightness":"5000","autofocus":"on","autoiris":"on"},"treeb":{"pan":"-176.45","tilt":"-13.00","zoom":"2000","focus":"8559","brightness":"5000","autofocus":"on","autoiris":"on"},"tree":{"pan":"-176.45","tilt":"-5.00","zoom":"300","focus":"9188","brightness":"5000","autofocus":"on","autoiris":"on"},"platforminside":{"pan":"154.54","tilt":"-13.00","zoom":"3799","focus":"8413","brightness":"5000","autofocus":"on","autoiris":"on"},"groundrc":{"pan":-129.31,"tilt":-34.4,"zoom":1,"focus":9503,"brightness":5000,"autofocus":"on","autoiris":"on"},"entry":{"pan":126.19,"tilt":-48.78,"zoom":1,"focus":9698,"brightness":5000,"autofocus":"on","autoiris":"on"},"insideperch":{"pan":148.54,"tilt":-7.25,"zoom":799,"focus":9733,"brightness":5000,"autofocus":"on","autoiris":"on"},"cache":{"pan":-153.81,"tilt":-25.89,"zoom":1299,"focus":8787,"brightness":5000,"autofocus":"on","autoiris":"on"},"inside":{"pan":143.19,"tilt":-28.78,"zoom":1,"focus":9627,"brightness":5000,"autofocus":"on","autoiris":"on"},"table":{"pan":135.54,"tilt":-24,"zoom":1798,"focus":8669,"brightness":5000,"autofocus":"on","autoiris":"on"},"groundlc":{"pan":174.18,"tilt":-36.78,"zoom":1,"focus":9716,"brightness":5000,"autofocus":"on","autoiris":"on"},"down":{"pan":153.19,"tilt":-73.78,"zoom":1,"focus":9556,"brightness":5000,"autofocus":"on","autoiris":"on"},"insidewater":{"pan":122.18,"tilt":-37.71,"zoom":1500,"focus":8297,"brightness":5000,"autofocus":"on","autoiris":"on"},"training":{"pan":-159.46,"tilt":-7.33,"zoom":937,"focus":8928,"brightness":5000,"autofocus":"on","autoiris":"on"},"temp":{"pan":-129.29,"tilt":-23.35,"zoom":9999,"focus":8736,"brightness":5000,"autofocus":"on","autoiris":"on"},"tmptoys":{"pan":-166.5,"tilt":-29.7,"zoom":498,"focus":8999,"brightness":5000,"autofocus":"on","autoiris":"on"},"escape":{"pan":-131.64,"tilt":-24.04,"zoom":3499,"focus":8584,"brightness":5000,"autofocus":"on","autoiris":"on"},"cornerc":{"pan":-100.22,"tilt":-4.26,"zoom":4498,"focus":8376,"brightness":5000,"autofocus":"off","autoiris":"on"},"tmp":{"pan":132.54,"tilt":-28,"zoom":1496,"focus":8480,"brightness":5000,"autofocus":"on","autoiris":"on"},"stickz":{"pan":-128.9,"tilt":-36.14,"zoom":4598,"focus":8307,"brightness":5000,"autofocus":"on","autoiris":"on"}},"isRoaming":false},"roach":{"presets":{"home":{"pan":"56.62","tilt":"-32.06","zoom":"1","iris":"5000","focus":"6933","autofocus":"on"},"stickb":{"pan":49.6,"tilt":-39.07,"zoom":1497,"focus":6756,"autofocus":"on"},"stickt":{"pan":63.61,"tilt":-13.08,"zoom":3001,"focus":6194,"autofocus":"on"},"down":{"pan":26.61,"tilt":-73.08,"zoom":1,"focus":9368,"autofocus":"on"},"left":{"pan":1.61,"tilt":-47.06,"zoom":1,"focus":8734,"autofocus":"on"},"leftcorner":{"pan":31.61,"tilt":-30.07,"zoom":1501,"focus":7694,"autofocus":"on"},"sticktl":{"pan":68.6,"tilt":-10.08,"zoom":5000,"focus":4913,"autofocus":"on"},"food":{"pan":96.6,"tilt":-88.5,"zoom":7001,"focus":3022,"autofocus":"on"},"food1":{"pan":35.81,"tilt":-37.79,"zoom":10409,"focus":4463,"autofocus":"on"},"food2":{"pan":22.36,"tilt":-40.31,"zoom":9349,"focus":4272,"autofocus":"on"},"temp":{"pan":57.6,"tilt":-43.87,"zoom":10909,"focus":3834,"autofocus":"on"},"top":{"pan":64.35,"tilt":-11.47,"zoom":6198,"focus":5357,"autofocus":"on"},"temp2":{"pan":61.85,"tilt":-31.01,"zoom":10069,"focus":4463,"autofocus":"on"}},"isRoaming":false},"cloudServer":"CloudSpaceServer","fox":{"presets":{"door":{"pan":90.65,"tilt":-15.12,"zoom":1,"focus":9751,"brightness":6000,"autofocus":"on","autoiris":"on"},"den":{"pan":60.01,"tilt":-37.55,"zoom":1,"focus":9645,"brightness":6000,"autofocus":"on","autoiris":"on"},"shade":{"pan":-37.97,"tilt":-15.55,"zoom":1798,"focus":9468,"brightness":6000,"autofocus":"on","autoiris":"on"},"center":{"pan":32.01,"tilt":-32.47,"zoom":1,"focus":9556,"brightness":6000,"autofocus":"on","autoiris":"on"},"bench":{"pan":"117.15","tilt":"-11.55","zoom":"4995","focus":"9188","brightness":"5000","autofocus":"on","autoiris":"on"},"table":{"pan":97.64,"tilt":-11.61,"zoom":1997,"focus":9339,"brightness":6000,"autofocus":"on","autoiris":"on"},"reed":{"pan":-33.09,"tilt":-4.86,"zoom":8910,"focus":8939,"brightness":6000,"autofocus":"on","autoiris":"on"},"fenn":{"pan":"-42.76","tilt":"-5.12","zoom":"5999","focus":"9015","brightness":"5000","autofocus":"on","autoiris":"on"},"platform2":{"pan":"-35.72","tilt":"-5.38","zoom":"2898","focus":"9376","brightness":"5000","autofocus":"on","autoiris":"on"},"bench2":{"pan":117.64,"tilt":-11.55,"zoom":7993,"focus":8785,"brightness":6000,"autofocus":"on","autoiris":"on"},"hole":{"pan":-36.09,"tilt":-17.14,"zoom":9999,"focus":8851,"brightness":6000,"autofocus":"on","autoiris":"on"},"hillwide":{"pan":"64.75","tilt":"-30.85","zoom":"1","focus":"9662","brightness":"5000","autofocus":"on","autoiris":"on"},"hillright":{"pan":"88.55","tilt":"-35.04","zoom":"342","focus":"9645","brightness":"5000","autofocus":"on","autoiris":"on"},"insidedoor":{"pan":72.25,"tilt":-11.95,"zoom":3334,"focus":9227,"brightness":6000,"autofocus":"on","autoiris":"on"},"leftfence":{"pan":-47.56,"tilt":-20.15,"zoom":900,"focus":9609,"brightness":6000,"autofocus":"on","autoiris":"on"},"treehouse":{"pan":-32.95,"tilt":-10.55,"zoom":1101,"focus":9609,"brightness":6000,"autofocus":"on","autoiris":"on"},"kaylatraining":{"pan":68.76,"tilt":-15.64,"zoom":2040,"focus":9539,"brightness":6000,"autofocus":"on","autoiris":"on"},"lindsaytraining":{"pan":-34.09,"tilt":-23.14,"zoom":1,"focus":9769,"brightness":6000,"autofocus":"on","autoiris":"on"},"hole3":{"pan":-40.74,"tilt":-15.52,"zoom":6498,"focus":8868,"brightness":6000,"autofocus":"on","autoiris":"on"},"hilltop":{"pan":63.75,"tilt":-26.85,"zoom":1927,"focus":9272,"brightness":6000,"autofocus":"on","autoiris":"on"},"benchhole":{"pan":116.15,"tilt":-17.25,"zoom":3775,"focus":9015,"brightness":6000,"autofocus":"on","autoiris":"on"},"grass":{"pan":-40.07,"tilt":-32.27,"zoom":101,"focus":9609,"brightness":6000,"autofocus":"on","autoiris":"on"},"hillleft":{"pan":26.92,"tilt":-32.5,"zoom":2159,"focus":8822,"brightness":6000,"autofocus":"on","autoiris":"on"},"treeclimb":{"pan":-38.76,"tilt":0,"zoom":897,"focus":9503,"brightness":6000,"autofocus":"on","autoiris":"on"},"hillrightw":{"pan":89.75,"tilt":-25.85,"zoom":1,"focus":9574,"brightness":6000,"autofocus":"on","autoiris":"on"},"treehouser":{"pan":-10.56,"tilt":-17.55,"zoom":1799,"focus":9398,"brightness":6000,"autofocus":"on","autoiris":"on"},"platforml":{"pan":-39.76,"tilt":-5.11,"zoom":6498,"focus":8944,"brightness":6000,"autofocus":"on","autoiris":"on"},"belowramp":{"pan":-28.41,"tilt":-17.8,"zoom":9999,"focus":8861,"brightness":6000,"autofocus":"on","autoiris":"on"},"belowplatform":{"pan":-33.26,"tilt":-16.14,"zoom":3295,"focus":9209,"brightness":6000,"autofocus":"on","autoiris":"on"},"down":{"pan":60.01,"tilt":-62.55,"zoom":1,"focus":9698,"brightness":6000,"autofocus":"on","autoiris":"on"},"belowramp2":{"pan":-25.82,"tilt":-22.79,"zoom":4001,"focus":8999,"brightness":6000,"autofocus":"on","autoiris":"on"},"tabletop":{"pan":97.64,"tilt":-7.61,"zoom":3395,"focus":9415,"brightness":6000,"autofocus":"on","autoiris":"on"},"fenndig":{"pan":-51.51,"tilt":-17.51,"zoom":3398,"focus":9113,"brightness":6000,"autofocus":"on","autoiris":"on"},"reedtemp":{"pan":-33.26,"tilt":-16.13,"zoom":9999,"focus":8917,"brightness":6000,"autofocus":"on","autoiris":"on"},"reedptemp":{"pan":-36.93,"tilt":-7.06,"zoom":9999,"focus":8818,"brightness":6000,"autofocus":"on","autoiris":"on"},"platform":{"pan":-34.4,"tilt":-4.86,"zoom":2596,"focus":9431,"brightness":6000,"autofocus":"on","autoiris":"on"},"left":{"pan":-34.06,"tilt":-15.55,"zoom":51,"focus":9539,"brightness":6000,"autofocus":"on","autoiris":"on"},"treehousel":{"pan":-48.06,"tilt":-15.55,"zoom":3797,"focus":9156,"brightness":6000,"autofocus":"on","autoiris":"on"},"home":{"pan":-24.82,"tilt":-17.79,"zoom":1,"focus":9609,"brightness":6000,"autofocus":"on","autoiris":"on"},"downright":{"pan":90.64,"tilt":-50.11,"zoom":1,"focus":9592,"brightness":6000,"autofocus":"on","autoiris":"on"},"downleft":{"pan":15.93,"tilt":-60.55,"zoom":51,"focus":9609,"brightness":6000,"autofocus":"on","autoiris":"on"},"temp":{"pan":-35.77,"tilt":-16.54,"zoom":6296,"focus":8985,"brightness":6000,"autofocus":"on","autoiris":"on"},"fenntemp":{"pan":-33.49,"tilt":-16.74,"zoom":9424,"focus":8933,"brightness":6000,"autofocus":"on","autoiris":"on"},"tmp":{"pan":43,"tilt":-48.95,"zoom":1508,"focus":9376,"brightness":6000,"autofocus":"on","autoiris":"on"},"tablebottom":{"pan":101.74,"tilt":-14.92,"zoom":3395,"focus":9301,"brightness":6000,"autofocus":"on","autoiris":"on"},"tmpreed":{"pan":-36.45,"tilt":-18.18,"zoom":9999,"focus":8815,"brightness":6000,"autofocus":"on","autoiris":"on"},"reedtmp":{"pan":-32.44,"tilt":-15.5,"zoom":9999,"focus":8946,"brightness":6000,"autofocus":"on","autoiris":"on"},"tmp2":{"pan":-27.06,"tilt":-14.55,"zoom":3048,"focus":9388,"brightness":6000,"autofocus":"on","autoiris":"on"},"fennf":{"pan":-36.09,"tilt":-6.4,"zoom":8502,"focus":8915,"brightness":6000,"autofocus":"on","autoiris":"on"},"reedf":{"pan":-28.42,"tilt":-5.86,"zoom":4594,"focus":9175,"brightness":6000,"autofocus":"on","autoiris":"on"},"rampl":{"pan":-30.06,"tilt":-21.83,"zoom":3051,"focus":9255,"brightness":6000,"autofocus":"on","autoiris":"on"},"rampt":{"pan":-31.32,"tilt":-6.86,"zoom":7592,"focus":8983,"brightness":6000,"autofocus":"on","autoiris":"on"},"right":{"pan":87.72,"tilt":-25.03,"zoom":1,"focus":9751,"brightness":6000,"autofocus":"on","autoiris":"on"},"brush":{"pan":-8.56,"tilt":-13.55,"zoom":2798,"focus":9288,"brightness":6000,"autofocus":"on","autoiris":"on"},"fennhole":{"pan":-19.56,"tilt":-17.55,"zoom":9999,"focus":8776,"brightness":6000,"autofocus":"on","autoiris":"on"}},"isRoaming":false,"roamTime":"300","roamSpeed":"50","roamIndex":1,"roamDirection":"forward","roamList":["fenn","belowramp"],"speed":"60"},"foxwideangle":{"presets":{},"isRoaming":false},"foxcorner":{"presets":{"den":{"pan":"-87.19","tilt":"10.00","zoom":"9999"},"table":{"pan":114.24,"tilt":16.89,"zoom":8330},"home":{"pan":"0.00","tilt":"0.00","zoom":"1"},"hillright":{"pan":-115.04,"tilt":-29.05,"zoom":9999},"training":{"pan":61.94,"tilt":-28.44,"zoom":8120},"insidedoor":{"pan":79.96,"tilt":0,"zoom":9999},"hillwide":{"pan":-79.22,"tilt":-0.11,"zoom":8999},"hillleft":{"pan":-123.42,"tilt":-0.11,"zoom":8999},"temp":{"pan":37.57,"tilt":-52.5,"zoom":6118},"bowl":{"pan":-19.96,"tilt":-68.57,"zoom":5000},"ellatraining":{"pan":26.79,"tilt":11.66,"zoom":9999},"training2":{"pan":-71.25,"tilt":-96.07,"zoom":7998}},"isRoaming":false},"timeRestrictionDisabled":false,"foxden":{"presets":{},"isRoaming":false},"customcam":["fullcammarmosetindoor","fullcampasture","fullcamparrot","fullcamfox","fullcamcrowoutdoor","fullcamwolf"],"customcambig":true,"hankcorner":{"presets":{},"isRoaming":false},"hank":{"presets":{"bottom":{"pan":-26.39,"tilt":-78.75,"zoom":1,"focus":218,"autofocus":"on"},"middle":{"pan":-26.39,"tilt":-59.7,"zoom":1,"focus":218,"autofocus":"on"},"top":{"pan":-26.39,"tilt":-43.76,"zoom":1,"focus":218,"autofocus":"on"}},"isRoaming":false},"customcamsbig":true,"customcamscommand":"customcamsbig","marmoset":{"presets":{"flaps":{"pan":111.73,"tilt":-7.08,"zoom":6096,"focus":8858,"brightness":5000,"autofocus":"on","autoiris":"on"},"domes":{"pan":"97.14","tilt":"-5.33","zoom":"2500","focus":"9031","brightness":"5000","autofocus":"on","autoiris":"on"},"platformleft":{"pan":79.12,"tilt":-0.5,"zoom":3000,"focus":9188,"brightness":5000,"autofocus":"on","autoiris":"on"},"platformright":{"pan":"107.14","tilt":"-15.33","zoom":"2000","focus":"9046","brightness":"5000","autofocus":"on","autoiris":"on"},"backdoor":{"pan":"140.13","tilt":"-10.33","zoom":"500","focus":"9574","brightness":"5000","autofocus":"on","autoiris":"on"},"frontdoor":{"pan":"70.13","tilt":"-25.33","zoom":"1","focus":"9698","brightness":"5000","autofocus":"on","autoiris":"on"},"ir":{"pan":14.96,"tilt":-3.33,"zoom":1,"focus":7096,"brightness":5000,"autofocus":"on","autoiris":"on"},"below":{"pan":"100.13","tilt":"-68.33","zoom":"1","focus":"9698","brightness":"5000","autofocus":"on","autoiris":"on"},"tire":{"pan":"76.73","tilt":"-6.72","zoom":"3999","focus":"9144","brightness":"5000","autofocus":"on","autoiris":"on"},"home":{"pan":100.13,"tilt":-11.33,"zoom":1,"focus":9857,"brightness":5000,"autofocus":"on","autoiris":"on"},"hose":{"pan":"62.32","tilt":"-43.33","zoom":"1818","focus":"8198","brightness":"5000","autofocus":"on","autoiris":"on"},"topshelfl":{"pan":"87.95","tilt":"0.00","zoom":"5999","focus":"8596","brightness":"5000","autofocus":"on","autoiris":"on"},"bottomshelfl":{"pan":86.98,"tilt":-12.24,"zoom":9904,"focus":8793,"brightness":5000,"autofocus":"on","autoiris":"on"},"right":{"pan":"120.18","tilt":"-6.60","zoom":"611","focus":"9645","brightness":"5000","autofocus":"on","autoiris":"on"},"table":{"pan":104.62,"tilt":-14.33,"zoom":3499,"focus":9131,"brightness":5000,"autofocus":"on","autoiris":"on"},"top":{"pan":"113.66","tilt":"0.00","zoom":"942","focus":"9539","brightness":"5000","autofocus":"on","autoiris":"on"},"banan":{"pan":"101.45","tilt":"-25.30","zoom":"1731","focus":"9141","brightness":"5000","autofocus":"on","autoiris":"on"},"bottomshelfr":{"pan":100.42,"tilt":-10.32,"zoom":9404,"focus":8878,"brightness":5000,"autofocus":"on","autoiris":"on"},"topshelfr":{"pan":99.93,"tilt":0,"zoom":9006,"focus":8732,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightpostb":{"pan":120.12,"tilt":-25.53,"zoom":3001,"focus":9093,"brightness":5000,"autofocus":"on","autoiris":"on"},"chins":{"pan":75.85,"tilt":-1.65,"zoom":9821,"focus":8465,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightshelves":{"pan":125.41,"tilt":-6.32,"zoom":2898,"focus":8973,"brightness":5000,"autofocus":"on","autoiris":"on"},"domel":{"pan":91.12,"tilt":-6.84,"zoom":7497,"focus":8787,"brightness":5000,"autofocus":"on","autoiris":"on"},"domer":{"pan":99.82,"tilt":-6,"zoom":8997,"focus":8860,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightscreen":{"pan":147.12,"tilt":-10.33,"zoom":1,"focus":9716,"brightness":5000,"autofocus":"on","autoiris":"on"},"leftpostshelves":{"pan":79.12,"tilt":-10,"zoom":3000,"focus":8976,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightshelvest":{"pan":120.41,"tilt":0,"zoom":2898,"focus":9117,"brightness":5000,"autofocus":"on","autoiris":"on"},"down":{"pan":100.12,"tilt":-61.33,"zoom":1,"focus":9928,"brightness":5000,"autofocus":"on","autoiris":"on"},"leftpostshelvesl":{"pan":76.33,"tilt":-19.47,"zoom":5772,"focus":8913,"brightness":5000,"autofocus":"on","autoiris":"on"},"centershelf":{"pan":109.78,"tilt":0,"zoom":3505,"focus":9068,"brightness":5000,"autofocus":"on","autoiris":"on"},"ground":{"pan":100.12,"tilt":-36.33,"zoom":1,"focus":9928,"brightness":5000,"autofocus":"on","autoiris":"on"},"groundl":{"pan":87.12,"tilt":-36.33,"zoom":1,"focus":9928,"brightness":5000,"autofocus":"on","autoiris":"on"},"groundr":{"pan":122.12,"tilt":-43.33,"zoom":1,"focus":9892,"brightness":5000,"autofocus":"on","autoiris":"on"},"temp":{"pan":121.5,"tilt":0,"zoom":7501,"focus":8782,"brightness":5000,"autofocus":"on","autoiris":"on"},"net":{"pan":104.12,"tilt":-5.33,"zoom":2200,"focus":9521,"brightness":5000,"autofocus":"on","autoiris":"on"},"leftscreen":{"pan":58.12,"tilt":-2,"zoom":1,"focus":9928,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightshelvesb":{"pan":124.4,"tilt":-12.82,"zoom":4397,"focus":8851,"brightness":5000,"autofocus":"on","autoiris":"on"},"left":{"pan":80.96,"tilt":-9.33,"zoom":1,"focus":9963,"brightness":5000,"autofocus":"on","autoiris":"on"},"flapl":{"pan":108.9,"tilt":-7.41,"zoom":10617,"focus":8845,"brightness":5000,"autofocus":"on","autoiris":"on"},"flapr":{"pan":114.96,"tilt":-7.4,"zoom":10331,"focus":8798,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightperch":{"pan":129.01,"tilt":-3.72,"zoom":4895,"focus":8829,"brightness":5000,"autofocus":"on","autoiris":"on"},"flapt":{"pan":111.63,"tilt":-6.33,"zoom":8134,"focus":8853,"brightness":5000,"autofocus":"on","autoiris":"on"}},"isRoaming":false},"marmosetindoor":{"presets":{"right":{"pan":"-22.02","tilt":"-25.19","zoom":"1","focus":"9609","brightness":"5000","autofocus":"on","autoiris":"on"},"windows":{"pan":"-50.02","tilt":"-25.19","zoom":"1500","focus":"9242","brightness":"5000","autofocus":"on","autoiris":"on"},"home":{"pan":"-65.02","tilt":"-30.19","zoom":"1","focus":"9769","brightness":"5000","autofocus":"on","autoiris":"on"},"flaps":{"pan":"-129.02","tilt":"-38.19","zoom":"1000","focus":"9468","brightness":"5000","autofocus":"on","autoiris":"on"},"domeleft":{"pan":"-93.02","tilt":"-19.19","zoom":"3000","focus":"8220","brightness":"5000","autofocus":"on","autoiris":"on"},"domeright":{"pan":"-32.02","tilt":"-27.69","zoom":"3000","focus":"8457","brightness":"5000","autofocus":"on","autoiris":"on"},"winr":{"pan":"-10.02","tilt":"-18.69","zoom":"3000","focus":"8268","brightness":"5000","autofocus":"on","autoiris":"on"},"left":{"pan":"-105.02","tilt":"-35.19","zoom":"1","focus":"9609","brightness":"5000","autofocus":"on","autoiris":"on"},"winl":{"pan":"-88.02","tilt":"-43.19","zoom":"3999","focus":"8117","brightness":"5000","autofocus":"on","autoiris":"on"},"bowll":{"pan":"-152.02","tilt":"-50.19","zoom":"500","focus":"9521","brightness":"5000","autofocus":"on","autoiris":"on"},"center":{"pan":"-78.02","tilt":"-35.19","zoom":"1","focus":"9716","brightness":"5000","autofocus":"on","autoiris":"on"},"bridger":{"pan":"-3.02","tilt":"-23.68","zoom":"1","focus":"9662","brightness":"5000","autofocus":"on","autoiris":"on"},"bridgel":{"pan":"176.97","tilt":"-38.68","zoom":"1","focus":"9556","brightness":"5000","autofocus":"on","autoiris":"on"},"win1":{"pan":"-87.93","tilt":"-44.10","zoom":"4107","focus":"8465","brightness":"5000","autofocus":"on","autoiris":"on"},"win2":{"pan":"-58.51","tilt":"-25.33","zoom":"4211","focus":"8459","brightness":"5000","autofocus":"on","autoiris":"on"},"win3":{"pan":-41.89,"tilt":-26.17,"zoom":4416,"focus":8447,"brightness":5000,"autofocus":"on","autoiris":"on"},"win4":{"pan":-10.41,"tilt":-18.67,"zoom":2717,"focus":8244,"brightness":5000,"autofocus":"on","autoiris":"on"},"down":{"pan":96.96,"tilt":-76.67,"zoom":1,"focus":9786,"brightness":5000,"autofocus":"on","autoiris":"on"},"table":{"pan":-68.61,"tilt":-32.96,"zoom":3000,"focus":8575,"brightness":5000,"autofocus":"on","autoiris":"on"},"bridgecl":{"pan":154.73,"tilt":-56.15,"zoom":224,"focus":9468,"brightness":5000,"autofocus":"on","autoiris":"on"},"topshelf":{"pan":-52.64,"tilt":-15.22,"zoom":2585,"focus":8569,"brightness":5000,"autofocus":"on","autoiris":"on"},"cornershelf":{"pan":-52.02,"tilt":-16.18,"zoom":2500,"focus":8779,"brightness":5000,"autofocus":"on","autoiris":"on"},"floor":{"pan":-65.02,"tilt":-64.18,"zoom":1,"focus":9963,"brightness":5000,"autofocus":"on","autoiris":"on"},"bowllc":{"pan":-152.02,"tilt":-55.64,"zoom":2000,"focus":8520,"brightness":5000,"autofocus":"on","autoiris":"on"},"pole":{"pan":-42.85,"tilt":-34.97,"zoom":300,"focus":9645,"brightness":5000,"autofocus":"on","autoiris":"on"},"bridgerc":{"pan":3.6,"tilt":-14.78,"zoom":2001,"focus":8441,"brightness":5000,"autofocus":"on","autoiris":"on"},"bridgem":{"pan":"16.97","tilt":"-53.68","zoom":"1","focus":"9521","brightness":"5000","autofocus":"on","autoiris":"on"},"flapl":{"pan":-122.52,"tilt":-34.65,"zoom":3000,"focus":8077,"brightness":5000,"autofocus":"on","autoiris":"on"}},"isRoaming":false,"speed":"80","roamTime":"2","roamIndex":2,"roamDirection":"forward","roamList":["home","right","bridger","bridgec","bridgel","left"],"roamSpeed":"90"},"layoutpresets":{"ncb":{"list":["fullcammarmoset","fullcampasture","fullcamparrot","fullcamfox","fullcamroach","fullcamcrowmulti"],"command":"customcamsbig"},"temporarylayoutsave":{"list":["fullcamchin","fullcampc","fullcamfox","fullcamparrot","fullcamcrow","fullcampasture"],"command":"customcamsbig"},"pasturenight":{"list":["fullcampasture","fullcamfox","fullcamfoxcorner","fullcamgeorgie","fullcamparrot","fullcamcrow"],"command":"customcamsbig"},"scnight":{"list":["fullcamnoodle","fullcampasture","fullcamfoxmulti3","fullcammarmoset","fullcamparrot","fullcamcrowmulti"],"command":"customcamsbig"},"temp":{"list":["fullcamwolf","fullcampasture","fullcamfox","fullcamgeorgie","fullcammarmosetmulti","fullcamcrowmulti"],"command":"customcamsbig"},"this":{"list":["fullcampasture","fullcamwolf","fullcamparrot","fullcamfox","fullcamcrowoutdoor","fullcammarmosetindoor"],"command":"customcamsbig"},"nightpc":{"list":["fullcampasture","fullcampc","fullcamfox","fullcamchin","fullcamcrow","fullcamparrot"],"command":"customcamsbig"}},"chin":{"presets":{},"isRoaming":false},"puppy":{"presets":{},"isRoaming":false},"bb":{"presets":{"a":{"pan":96.76,"tilt":-38.54,"zoom":10001,"focus":2481,"autofocus":"on"},"b":{"pan":96.76,"tilt":-51.56,"zoom":9700,"focus":1486,"autofocus":"on"},"c":{"pan":109.76,"tilt":-58.57,"zoom":9700,"focus":1086,"autofocus":"on"},"backcorner":{"pan":66.76,"tilt":-88.57,"zoom":1,"focus":1599,"autofocus":"on"},"food":{"pan":-53.23,"tilt":-81.56,"zoom":10501,"focus":41,"autofocus":"on"},"topright":{"pan":92.75,"tilt":-43.57,"zoom":3001,"focus":1950,"autofocus":"on"},"backleft":{"pan":17.75,"tilt":-58.57,"zoom":999,"focus":1509,"autofocus":"on"},"temp":{"pan":77.75,"tilt":-88.57,"zoom":999,"focus":792,"autofocus":"on"},"backright":{"pan":-84,"tilt":-84.56,"zoom":1497,"focus":1637,"autofocus":"on"},"backcenter":{"pan":-32.25,"tilt":-71.58,"zoom":1497,"focus":2150,"autofocus":"on"},"home":{"pan":66.75,"tilt":-58.57,"zoom":1,"focus":1048,"autofocus":"on"},"nest":{"pan":2.14,"tilt":-57.29,"zoom":10146,"focus":1452,"autofocus":"on"},"nest2":{"pan":110.75,"tilt":-56.58,"zoom":9001,"focus":1373,"autofocus":"on"},"food1":{"pan":84.25,"tilt":-61.64,"zoom":11001,"focus":852,"autofocus":"on"},"food2":{"pan":97.25,"tilt":-38.92,"zoom":12001,"focus":2511,"autofocus":"on"},"food3":{"pan":68.87,"tilt":-46.72,"zoom":11411,"focus":1784,"autofocus":"on"},"food4":{"pan":46.75,"tilt":-47.66,"zoom":10787,"focus":1520,"autofocus":"on"},"food5":{"pan":46.75,"tilt":-45.33,"zoom":10287,"focus":1784,"autofocus":"on"},"tmp":{"pan":46.76,"tilt":-45.33,"zoom":10287,"focus":1784,"autofocus":"on"}},"isRoaming":false,"roamTime":"30","roamSpeed":"10","roamIndex":1,"roamDirection":"forward","roamList":["a","b","c"]},"marty":{"presets":{"a":{"pan":121.78,"tilt":-34.04,"zoom":7999,"focus":1875,"autofocus":"on"},"b":{"pan":141.77,"tilt":-47.54,"zoom":7999,"focus":1324,"autofocus":"on"},"c":{"pan":176.78,"tilt":-45.03,"zoom":7999,"focus":460,"autofocus":"on"},"food1":{"pan":130.31,"tilt":-46.04,"zoom":10909,"focus":1286,"autofocus":"on"},"food2":{"pan":106.8,"tilt":-70.53,"zoom":10909,"focus":41,"autofocus":"on"},"bowltemp":{"pan":104.97,"tilt":-45.18,"zoom":10499,"focus":1818,"autofocus":"on"},"party":{"pan":126.57,"tilt":-62.17,"zoom":7912,"focus":177,"autofocus":"on"},"food3":{"pan":61.8,"tilt":-60,"zoom":10407,"focus":41,"autofocus":"on"},"food5":{"pan":104.81,"tilt":-50.88,"zoom":10909,"focus":954,"autofocus":"on"},"food4":{"pan":29.81,"tilt":-86.02,"zoom":9907,"focus":88,"autofocus":"on"}},"isRoaming":false,"roamTime":"10","roamSpeed":"10","roamIndex":-1,"roamDirection":"forward","roamList":[]},"georgiewater":{"presets":{},"isRoaming":false},"construction":{"presets":{},"isRoaming":false},"wolf":{"presets":{"home":{"pan":-114.17,"tilt":-9.99,"zoom":1,"focus":4084,"brightness":5000,"autofocus":"on","autoiris":"on"},"river":{"pan":-154.17,"tilt":-9.99,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"body":{"pan":-99.46,"tilt":-1.09,"zoom":4998,"focus":7943,"brightness":5000,"autofocus":"on","autoiris":"on"},"play":{"pan":-95.09,"tilt":-1.84,"zoom":1001,"focus":7129,"brightness":5000,"autofocus":"on","autoiris":"on"},"right":{"pan":-111.91,"tilt":-2.49,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"left":{"pan":154.85,"tilt":-14.98,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"den1":{"pan":-158.66,"tilt":-2.42,"zoom":399,"focus":5131,"brightness":5000,"autofocus":"on","autoiris":"on"},"pond":{"pan":-169.66,"tilt":-10.99,"zoom":368,"focus":6303,"brightness":5000,"autofocus":"on","autoiris":"on"},"den1t":{"pan":-156.64,"tilt":1.86,"zoom":797,"focus":6659,"brightness":5000,"autofocus":"on","autoiris":"on"},"den1ramp":{"pan":-166.33,"tilt":-1.39,"zoom":597,"focus":6102,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2r":{"pan":-92.94,"tilt":-0.97,"zoom":3093,"focus":7909,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2brush":{"pan":-91.44,"tilt":-1.17,"zoom":3093,"focus":7932,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2":{"pan":-95.34,"tilt":-0.16,"zoom":1489,"focus":7511,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2b":{"pan":-95.58,"tilt":-1.03,"zoom":3993,"focus":7964,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2m":{"pan":-95.34,"tilt":0.24,"zoom":3492,"focus":7949,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2t":{"pan":-96.04,"tilt":1.22,"zoom":2991,"focus":7892,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2lc":{"pan":-99.2,"tilt":-1.1,"zoom":4788,"focus":7957,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2l":{"pan":-98.33,"tilt":-0.57,"zoom":2289,"focus":7813,"brightness":5000,"autofocus":"on","autoiris":"on"},"farcorner":{"pan":-104.33,"tilt":-0.59,"zoom":983,"focus":6990,"brightness":5000,"autofocus":"on","autoiris":"on"},"bigrocks":{"pan":-115.81,"tilt":-0.76,"zoom":801,"focus":6561,"brightness":5000,"autofocus":"on","autoiris":"on"},"grass":{"pan":-95.31,"tilt":-2.17,"zoom":1095,"focus":6858,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2w":{"pan":-96.93,"tilt":-0.33,"zoom":1239,"focus":7391,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2rw":{"pan":-90.66,"tilt":-1.11,"zoom":997,"focus":7120,"brightness":5000,"autofocus":"on","autoiris":"on"},"belowplatform":{"pan":-88.85,"tilt":-1.13,"zoom":1794,"focus":7627,"brightness":5000,"autofocus":"on","autoiris":"on"},"center":{"pan":-106.62,"tilt":-4.37,"zoom":199,"focus":4122,"brightness":5000,"autofocus":"on","autoiris":"on"},"den1r":{"pan":-136.66,"tilt":-2.42,"zoom":298,"focus":5869,"brightness":5000,"autofocus":"on","autoiris":"on"},"den1l":{"pan":176.41,"tilt":-3.41,"zoom":296,"focus":4550,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2ramp":{"pan":-97.68,"tilt":-0.52,"zoom":2989,"focus":7897,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2mc":{"pan":-94.75,"tilt":0.04,"zoom":7492,"focus":7953,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightcorner":{"pan":-87.69,"tilt":-1.32,"zoom":4290,"focus":7972,"brightness":5000,"autofocus":"on","autoiris":"on"},"leftcorner":{"pan":115.27,"tilt":-22.74,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"downright":{"pan":-114.58,"tilt":-25.53,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"down":{"pan":-174.15,"tilt":-60,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"downleft":{"pan":128.47,"tilt":-38.25,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"farfence":{"pan":-96.93,"tilt":-0.33,"zoom":397,"focus":5082,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightfence":{"pan":-91.21,"tilt":-5.25,"zoom":300,"focus":4843,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2bc":{"pan":-95.87,"tilt":-1.13,"zoom":7993,"focus":7964,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightcornerc":{"pan":-87.43,"tilt":-1.33,"zoom":6093,"focus":8006,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2base":{"pan":-95.64,"tilt":-0.42,"zoom":2188,"focus":7715,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2wr":{"pan":-93.69,"tilt":-0.98,"zoom":646,"focus":6243,"brightness":5000,"autofocus":"on","autoiris":"on"},"waterfall1":{"pan":-130.16,"tilt":-8.39,"zoom":6000,"focus":7272,"brightness":5000,"autofocus":"on","autoiris":"on"},"waterfall2":{"pan":-159.66,"tilt":-12.39,"zoom":4368,"focus":6851,"brightness":5000,"autofocus":"on","autoiris":"on"},"tmptimber":{"pan":-154.93,"tilt":1.7,"zoom":2794,"focus":7501,"brightness":5000,"autofocus":"on","autoiris":"on"},"backleftcorner":{"pan":164.41,"tilt":-3.41,"zoom":795,"focus":6080,"brightness":5000,"autofocus":"on","autoiris":"on"},"den1inside":{"pan":-157.75,"tilt":-5.9,"zoom":2098,"focus":7218,"brightness":5000,"autofocus":"on","autoiris":"on"},"switchgater":{"pan":157.07,"tilt":-4.98,"zoom":998,"focus":6906,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2gap":{"pan":-96.87,"tilt":-1.04,"zoom":7993,"focus":7966,"brightness":5000,"autofocus":"on","autoiris":"on"},"awatmp":{"pan":-93.86,"tilt":-2.1,"zoom":3094,"focus":7712,"brightness":5000,"autofocus":"on","autoiris":"on"},"den1tz":{"pan":-154.63,"tilt":1.95,"zoom":1296,"focus":7266,"brightness":5000,"autofocus":"on","autoiris":"on"},"grassl":{"pan":-100.17,"tilt":-1.6,"zoom":1095,"focus":7307,"brightness":5000,"autofocus":"on","autoiris":"on"},"timbertmp":{"pan":-92.93,"tilt":-0.96,"zoom":992,"focus":6652,"brightness":5000,"autofocus":"on","autoiris":"on"},"centershade":{"pan":-105.22,"tilt":-2.59,"zoom":2999,"focus":7675,"brightness":5000,"autofocus":"on","autoiris":"on"},"tmp":{"pan":-154.16,"tilt":-8.39,"zoom":1500,"focus":6684,"brightness":5000,"autofocus":"on","autoiris":"on"},"wolfcornercam":{"pan":-90.86,"tilt":2.02,"zoom":528,"focus":5326,"brightness":5000,"autofocus":"on","autoiris":"on"}},"isRoaming":false,"speed":"40","roamTime":"10","roamSpeed":"50","roamIndex":5,"roamDirection":"forward","roamList":["rightfence","belowplatform","den2rw","den2w","bigrocks","den1r","den1l","left","river"]},"wolfden":{"presets":{"home":{"pan":0,"tilt":0,"zoom":1},"rightcorner":{"pan":0,"tilt":-9.83,"zoom":3001,"focus":1}},"isRoaming":false},"wolfcorner":{"presets":{"funnelspider":{"pan":15.58,"tilt":-26.51,"zoom":10909,"focus":6322,"brightness":5000,"autofocus":"on","autoiris":"on"},"home":{"pan":15.76,"tilt":-19.31,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2m":{"pan":15.76,"tilt":-19.31,"zoom":300,"focus":748,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2t":{"pan":2.81,"tilt":-8.83,"zoom":700,"focus":5396,"brightness":5000,"autofocus":"on","autoiris":"on"},"belowplatform":{"pan":-139.25,"tilt":-47.01,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightfence":{"pan":81.96,"tilt":-25.78,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"leftfence":{"pan":-37.64,"tilt":-17.57,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"body":{"pan":5.14,"tilt":-15.84,"zoom":999,"focus":6134,"brightness":5000,"autofocus":"on","autoiris":"on"},"feed":{"pan":-45.28,"tilt":-27.92,"zoom":1,"focus":2213,"brightness":5000,"autofocus":"on","autoiris":"on"},"rampr":{"pan":26.04,"tilt":-15.63,"zoom":599,"focus":5288,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2b":{"pan":32.27,"tilt":-18.31,"zoom":1597,"focus":6129,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2door":{"pan":19.78,"tilt":-35.14,"zoom":244,"focus":2657,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2inside":{"pan":19.78,"tilt":-32.33,"zoom":945,"focus":6086,"brightness":5000,"autofocus":"on","autoiris":"on"},"backfence":{"pan":61.74,"tilt":-9.8,"zoom":249,"focus":3693,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2":{"pan":15.75,"tilt":-23.31,"zoom":1,"focus":2213,"brightness":5000,"autofocus":"on","autoiris":"on"},"belowrampl":{"pan":5.48,"tilt":-15.31,"zoom":1046,"focus":6363,"brightness":5000,"autofocus":"on","autoiris":"on"},"belowrampr":{"pan":35.47,"tilt":-24.1,"zoom":397,"focus":2012,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2l":{"pan":-28.57,"tilt":-34.56,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"left":{"pan":-22.58,"tilt":-16.59,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"wide":{"pan":-8.51,"tilt":-11.2,"zoom":1,"focus":2652,"brightness":5000,"autofocus":"on","autoiris":"on"},"brushl":{"pan":-27,"tilt":-10.99,"zoom":500,"focus":5679,"brightness":5000,"autofocus":"on","autoiris":"on"},"brushlc":{"pan":-28,"tilt":-12,"zoom":1400,"focus":7302,"brightness":5000,"autofocus":"on","autoiris":"on"},"down":{"pan":74.81,"tilt":-49.3,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"belowplatforml":{"pan":154.81,"tilt":-57.31,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"belowplatformr":{"pan":-54.62,"tilt":-40.38,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"water":{"pan":-121.23,"tilt":-42.67,"zoom":247,"focus":2657,"brightness":5000,"autofocus":"on","autoiris":"on"},"downleft":{"pan":14.77,"tilt":-54.29,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"downright":{"pan":102.93,"tilt":-49.3,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"right":{"pan":54.75,"tilt":-21.3,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"middle":{"pan":58.33,"tilt":-28.82,"zoom":1,"focus":2213,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2entrance":{"pan":42.91,"tilt":-40.66,"zoom":1,"focus":2945,"brightness":5000,"autofocus":"on","autoiris":"on"},"grass":{"pan":-22.59,"tilt":-6.54,"zoom":1298,"focus":7247,"brightness":5000,"autofocus":"on","autoiris":"on"},"river":{"pan":-22.47,"tilt":-3.32,"zoom":2895,"focus":7949,"brightness":5000,"autofocus":"on","autoiris":"on"},"leftcorner":{"pan":-29.47,"tilt":-3.32,"zoom":2895,"focus":7869,"brightness":5000,"autofocus":"on","autoiris":"on"},"leftcornerc":{"pan":-29.27,"tilt":-3.14,"zoom":6895,"focus":7997,"brightness":5000,"autofocus":"on","autoiris":"on"},"center":{"pan":-14,"tilt":-9.49,"zoom":500,"focus":5229,"brightness":5000,"autofocus":"on","autoiris":"on"},"den1":{"pan":-14.14,"tilt":-2.58,"zoom":1700,"focus":7348,"brightness":5000,"autofocus":"on","autoiris":"on"},"grassc":{"pan":-22.59,"tilt":-6.75,"zoom":2797,"focus":7786,"brightness":5000,"autofocus":"on","autoiris":"on"},"back":{"pan":18.21,"tilt":-10.43,"zoom":397,"focus":6303,"brightness":5000,"autofocus":"on","autoiris":"on"},"burmr":{"pan":38.9,"tilt":-17.23,"zoom":1100,"focus":6162,"brightness":5000,"autofocus":"on","autoiris":"on"},"burm":{"pan":26.53,"tilt":-15.59,"zoom":298,"focus":3818,"brightness":5000,"autofocus":"on","autoiris":"on"},"rampl":{"pan":11.97,"tilt":-14.47,"zoom":797,"focus":6216,"brightness":5000,"autofocus":"on","autoiris":"on"},"nearcorner":{"pan":-157.11,"tilt":-38.54,"zoom":276,"focus":3248,"brightness":5000,"autofocus":"on","autoiris":"on"},"den2shade":{"pan":-1.3,"tilt":-19.27,"zoom":500,"focus":5522,"brightness":5000,"autofocus":"on","autoiris":"on"},"rightcorner":{"pan":70.81,"tilt":-9.8,"zoom":948,"focus":6823,"brightness":5000,"autofocus":"on","autoiris":"on"},"belowramprc":{"pan":33.46,"tilt":-26.8,"zoom":997,"focus":5863,"brightness":5000,"autofocus":"on","autoiris":"on"},"spider":{"pan":-120.9,"tilt":-25.86,"zoom":1,"focus":748,"brightness":5000,"autofocus":"on","autoiris":"on"},"spider2":{"pan":7.73,"tilt":-13.37,"zoom":10909,"focus":6897,"brightness":5000,"autofocus":"on","autoiris":"on"},"wolfcam":{"pan":-30.63,"tilt":-1.57,"zoom":6000,"focus":8022,"brightness":5000,"autofocus":"on","autoiris":"on"},"belowramplz":{"pan":8.08,"tilt":-15.6,"zoom":1343,"focus":6507,"brightness":5000,"autofocus":"on","autoiris":"on"},"grassr":{"pan":-20.07,"tilt":-6.04,"zoom":1298,"focus":7309,"brightness":5000,"autofocus":"on","autoiris":"on"},"awatmp":{"pan":-22.3,"tilt":-6.22,"zoom":10409,"focus":7809,"brightness":5000,"autofocus":"on","autoiris":"on"},"timbertmp":{"pan":-22.75,"tilt":-6.16,"zoom":5295,"focus":7794,"brightness":5000,"autofocus":"on","autoiris":"on"},"centershade":{"pan":-19.98,"tilt":-5.11,"zoom":3001,"focus":7869,"brightness":5000,"autofocus":"on","autoiris":"on"},"tmp":{"pan":-26.61,"tilt":-5.44,"zoom":1298,"focus":7164,"brightness":5000,"autofocus":"on","autoiris":"on"}},"isRoaming":false},"wolfden2":{"presets":{"bottomright":{"pan":0,"tilt":0,"zoom":1},"home":{"pan":0,"tilt":0,"zoom":1},"rightcorner":{"pan":65.06,"tilt":-19.83,"zoom":5000},"leftcorner":{"pan":0,"tilt":82.5,"zoom":5000}},"isRoaming":false}} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..974b6dc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3582 @@ +{ + "name": "alveuscontroller", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "alveuscontroller", + "version": "0.1.0", + "license": "UNLICENSED", + "dependencies": { + "@trycourier/courier": "^4.1.0", + "@twurple/api": "^5.2.7", + "@twurple/auth": "^5.2.7", + "@twurple/chat": "^5.2.7", + "@twurple/eventsub": "^5.2.7", + "digest-fetch": "^2.0.1", + "dotenv": "^16.0.3", + "obs-websocket-js": "^5.0.2", + "obs-websocket-js-27": "npm:obs-websocket-js@^4.0.3", + "on-change": "^4.0.2", + "osc": "^2.4.4", + "unifi-client": "^0.11.0" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "nodemon": "^3.0.1", + "prettier": "^3.0.1" + } + }, + "node_modules/@d-fischer/cache-decorators": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "@d-fischer/shared-utils": "^3.0.1", + "@types/node": "^14.14.22", + "tslib": "^2.1.0" + } + }, + "node_modules/@d-fischer/connection": { + "version": "7.0.1", + "license": "MIT", + "dependencies": { + "@d-fischer/isomorphic-ws": "^7.0.0", + "@d-fischer/logger": "^4.2.0", + "@d-fischer/shared-utils": "^3.3.0", + "@d-fischer/typed-event-emitter": "^3.3.0", + "@types/node": "^16.7.10", + "@types/ws": "^8.5.3", + "tslib": "^2.4.1", + "ws": "^8.11.0" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/connection/node_modules/@types/node": { + "version": "16.18.10", + "license": "MIT" + }, + "node_modules/@d-fischer/cross-fetch": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/@d-fischer/deprecate": { + "version": "2.0.2", + "license": "MIT" + }, + "node_modules/@d-fischer/detect-node": { + "version": "3.0.1", + "license": "MIT" + }, + "node_modules/@d-fischer/escape-string-regexp": { + "version": "5.0.0", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@d-fischer/isomorphic-ws": { + "version": "7.0.0", + "license": "MIT", + "peerDependencies": { + "ws": "^8.2.0" + } + }, + "node_modules/@d-fischer/logger": { + "version": "4.2.0", + "license": "MIT", + "dependencies": { + "@d-fischer/shared-utils": "^3.2.0", + "detect-node": "^2.0.4", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/promise.allsettled": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "array.prototype.map": "^1.0.3", + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2", + "get-intrinsic": "^1.0.2", + "iterate-value": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/qs": { + "version": "7.0.2", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/rate-limiter": { + "version": "0.6.1", + "license": "MIT", + "dependencies": { + "@d-fischer/logger": "^4.0.0", + "@d-fischer/promise.allsettled": "^2.0.2", + "@d-fischer/shared-utils": "^3.2.0", + "@types/node": "^12.12.5", + "tslib": "^2.0.3" + } + }, + "node_modules/@d-fischer/rate-limiter/node_modules/@types/node": { + "version": "12.20.55", + "license": "MIT" + }, + "node_modules/@d-fischer/raw-body": { + "version": "2.4.3", + "license": "MIT", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@d-fischer/shared-utils": { + "version": "3.4.0", + "license": "MIT", + "dependencies": { + "@types/node": "^14.11.2", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@d-fischer/typed-event-emitter": { + "version": "3.3.0", + "license": "MIT", + "dependencies": { + "@types/node": "^14.11.2", + "tslib": "^2.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "2.8.0", + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@serialport/binding-mock": { + "version": "10.2.2", + "license": "MIT", + "optional": true, + "dependencies": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@serialport/bindings-cpp": { + "version": "10.8.0", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "^10.2.1", + "debug": "^4.3.2", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=12.17.0 <13.0 || >=14.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-interface": { + "version": "1.2.2", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/@serialport/parser-byte-length": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-cctalk": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-delimiter": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-inter-byte-timeout": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-packet-length": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@serialport/parser-readline": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "dependencies": { + "@serialport/parser-delimiter": "10.5.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-ready": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-regex": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-slip-encoder": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-spacepacket": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "debug": "^4.3.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@trycourier/courier": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@twurple/api": { + "version": "5.2.7", + "license": "MIT", + "dependencies": { + "@d-fischer/cache-decorators": "^3.0.0", + "@d-fischer/detect-node": "^3.0.1", + "@d-fischer/logger": "^4.0.0", + "@d-fischer/rate-limiter": "^0.6.1", + "@d-fischer/shared-utils": "^3.2.0", + "@twurple/api-call": "^5.2.7", + "@twurple/common": "^5.2.7", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "@twurple/auth": "^5.0.0" + } + }, + "node_modules/@twurple/api-call": { + "version": "5.2.7", + "license": "MIT", + "dependencies": { + "@d-fischer/cross-fetch": "^4.0.2", + "@d-fischer/qs": "^7.0.2", + "@twurple/common": "^5.2.7", + "@types/node-fetch": "^2.5.7", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@twurple/auth": { + "version": "5.2.7", + "license": "MIT", + "dependencies": { + "@d-fischer/logger": "^4.0.0", + "@d-fischer/shared-utils": "^3.2.0", + "@twurple/api-call": "^5.2.7", + "@twurple/common": "^5.2.7", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@twurple/chat": { + "version": "5.2.7", + "license": "MIT", + "dependencies": { + "@d-fischer/cache-decorators": "^3.0.0", + "@d-fischer/deprecate": "^2.0.2", + "@d-fischer/logger": "^4.0.0", + "@d-fischer/rate-limiter": "^0.6.1", + "@d-fischer/shared-utils": "^3.2.0", + "@d-fischer/typed-event-emitter": "^3.2.2", + "@twurple/common": "^5.2.7", + "ircv3": "^0.29.7", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "@twurple/auth": "^5.0.0" + } + }, + "node_modules/@twurple/common": { + "version": "5.2.7", + "license": "MIT", + "dependencies": { + "@d-fischer/shared-utils": "^3.2.0", + "klona": "^2.0.4", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/@twurple/eventsub": { + "version": "5.2.7", + "license": "MIT", + "dependencies": { + "@d-fischer/logger": "^4.0.0", + "@d-fischer/raw-body": "^2.4.3", + "@d-fischer/shared-utils": "^3.2.0", + "@d-fischer/typed-event-emitter": "^3.2.2", + "@twurple/auth": "^5.2.7", + "@twurple/common": "^5.2.7", + "@types/express-serve-static-core": "^4.17.24", + "httpanda": "^0.4.6", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "@twurple/api": "^5.0.0" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.31", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/node": { + "version": "14.18.35", + "license": "MIT" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.2", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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/array.prototype.map": { + "version": "1.0.5", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-curlirize": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/axios-curlirize/-/axios-curlirize-1.3.7.tgz", + "integrity": "sha512-csSsuMyZj1dv1fL0zRPnDAHWrmlISMvK+wx9WJI/igRVDT4VMgbf2AVenaHghFLfI1nQijXUevYEguYV6u5hjA==" + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true + }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/bytes": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "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" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "license": "MIT", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "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==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, + "node_modules/crypto-js": { + "version": "4.1.1", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.4", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/digest-fetch": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-2.0.3.tgz", + "integrity": "sha512-HuTjHQE+wplAR+H8/YGwQjIGR1RQUCEsQcRyp3dZfuuxpSQH4OTm4BkHxyXuzxwmxUrNVzIPf9XkXi8QMJDNwQ==", + "dependencies": { + "base-64": "^0.1.0", + "js-sha256": "^0.9.0", + "js-sha512": "^0.8.0", + "md5": "^2.3.0" + } + }, + "node_modules/dotenv": { + "version": "16.0.3", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/es-abstract": { + "version": "1.20.5", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/es-get-iterator": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.0", + "has-symbols": "^1.0.1", + "is-arguments": "^1.1.0", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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", + "license": "MIT" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "1.7.3", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/httpanda": { + "version": "0.4.6", + "license": "MIT", + "dependencies": { + "@types/node": "^14.11.2", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ircv3": { + "version": "0.29.10", + "license": "MIT", + "dependencies": { + "@d-fischer/connection": "^7.0.0", + "@d-fischer/escape-string-regexp": "^5.0.0", + "@d-fischer/logger": "^4.0.0", + "@d-fischer/shared-utils": "^3.0.1", + "@d-fischer/typed-event-emitter": "^3.3.0", + "@types/node": "^14.14.19", + "klona": "^2.0.4", + "tslib": "^2.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "license": "MIT", + "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-bigint": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "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-boolean-object": { + "version": "1.1.2", + "license": "MIT", + "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-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "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-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-map": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-number-object": { + "version": "1.0.7", + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "license": "MIT", + "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-set": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/iterate-iterator": { + "version": "1.0.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/iterate-value": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "node_modules/js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/klona": { + "version": "2.0.5", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/long": { + "version": "4.0.0", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/ms": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.6.0", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nodemon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.1.tgz", + "integrity": "sha512-k43xGaDtaDIcufn0Fc6fTtsdKSkV/hQzoQFigNH//GaKta28yoKVYXCnV+KXRqfT/YzsFaQU9VdeEG+HEyxr6A==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "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/object-inspect": { + "version": "1.12.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obs-websocket-js": { + "version": "5.0.2", + "license": "MIT", + "dependencies": { + "@msgpack/msgpack": "^2.7.1", + "crypto-js": "^4.1.1", + "debug": "^4.3.2", + "eventemitter3": "^4.0.7", + "isomorphic-ws": "^4.0.1", + "type-fest": "^2.3.2", + "ws": "^8.2.2" + }, + "engines": { + "node": ">12.0" + } + }, + "node_modules/obs-websocket-js-27": { + "name": "obs-websocket-js", + "version": "4.0.3", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "isomorphic-ws": "^4.0.1", + "sha.js": "^2.4.9", + "ws": "^7.2.0" + } + }, + "node_modules/obs-websocket-js-27/node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/obs-websocket-js-27/node_modules/ws": { + "version": "7.5.9", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/on-change": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/on-change/-/on-change-4.0.2.tgz", + "integrity": "sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/on-change?sponsor=1" + } + }, + "node_modules/osc": { + "version": "2.4.4", + "license": "(MIT OR GPL-2.0)", + "dependencies": { + "long": "4.0.0", + "slip": "1.0.2", + "wolfy87-eventemitter": "5.2.9", + "ws": "8.13.0" + }, + "optionalDependencies": { + "serialport": "10.5.0" + } + }, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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/prettier": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz", + "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "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/regexp.prototype.flags": { + "version": "1.4.3", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialport": { + "version": "10.5.0", + "license": "MIT", + "optional": true, + "dependencies": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "10.8.0", + "@serialport/parser-byte-length": "10.5.0", + "@serialport/parser-cctalk": "10.5.0", + "@serialport/parser-delimiter": "10.5.0", + "@serialport/parser-inter-byte-timeout": "10.5.0", + "@serialport/parser-packet-length": "10.5.0", + "@serialport/parser-readline": "10.5.0", + "@serialport/parser-ready": "10.5.0", + "@serialport/parser-regex": "10.5.0", + "@serialport/parser-slip-encoder": "10.5.0", + "@serialport/parser-spacepacket": "10.5.0", + "@serialport/stream": "10.5.0", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slip": { + "version": "1.0.2", + "license": "(MIT OR GPL-2.0)" + }, + "node_modules/statuses": { + "version": "1.5.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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/toidentifier": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.4.1", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/unifi-client": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/unifi-client/-/unifi-client-0.11.0.tgz", + "integrity": "sha512-H1tdd1joGuaFvonw5vOnWgWJkXL93qIr2pfO6UHPJld27F9PO//q7Uz3gbQ6lauEHApswLBG8UPw+QiJ+rGAeg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/thib3113/unifi-client" + }, + { + "type": "individual", + "url": "https://paypal.me/thib3113" + } + ], + "dependencies": { + "axios": "1.6.1", + "axios-curlirize": "1.3.7", + "cookie": "0.6.0", + "debug": "4.3.4", + "jsonwebtoken": "9.0.2", + "semver": "7.5.4", + "set-cookie-parser": "2.6.0", + "ws": "8.14.2" + }, + "engines": { + "node": ">= 14" + }, + "optionalDependencies": { + "bufferutil": "^4.0.8", + "utf-8-validate": "^6.0.3" + } + }, + "node_modules/unifi-client/node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "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/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utf-8-validate": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.3.tgz", + "integrity": "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wolfy87-eventemitter": { + "version": "5.2.9", + "license": "Unlicense" + }, + "node_modules/ws": { + "version": "8.13.0", + "license": "MIT", + "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/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + }, + "dependencies": { + "@d-fischer/cache-decorators": { + "version": "3.0.0", + "requires": { + "@d-fischer/shared-utils": "^3.0.1", + "@types/node": "^14.14.22", + "tslib": "^2.1.0" + } + }, + "@d-fischer/connection": { + "version": "7.0.1", + "requires": { + "@d-fischer/isomorphic-ws": "^7.0.0", + "@d-fischer/logger": "^4.2.0", + "@d-fischer/shared-utils": "^3.3.0", + "@d-fischer/typed-event-emitter": "^3.3.0", + "@types/node": "^16.7.10", + "@types/ws": "^8.5.3", + "tslib": "^2.4.1", + "ws": "^8.11.0" + }, + "dependencies": { + "@types/node": { + "version": "16.18.10" + } + } + }, + "@d-fischer/cross-fetch": { + "version": "4.1.0", + "requires": { + "node-fetch": "2.6.7" + } + }, + "@d-fischer/deprecate": { + "version": "2.0.2" + }, + "@d-fischer/detect-node": { + "version": "3.0.1" + }, + "@d-fischer/escape-string-regexp": { + "version": "5.0.0" + }, + "@d-fischer/isomorphic-ws": { + "version": "7.0.0", + "requires": {} + }, + "@d-fischer/logger": { + "version": "4.2.0", + "requires": { + "@d-fischer/shared-utils": "^3.2.0", + "detect-node": "^2.0.4", + "tslib": "^2.0.3" + } + }, + "@d-fischer/promise.allsettled": { + "version": "2.0.2", + "requires": { + "array.prototype.map": "^1.0.3", + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.2", + "get-intrinsic": "^1.0.2", + "iterate-value": "^1.0.2" + } + }, + "@d-fischer/qs": { + "version": "7.0.2" + }, + "@d-fischer/rate-limiter": { + "version": "0.6.1", + "requires": { + "@d-fischer/logger": "^4.0.0", + "@d-fischer/promise.allsettled": "^2.0.2", + "@d-fischer/shared-utils": "^3.2.0", + "@types/node": "^12.12.5", + "tslib": "^2.0.3" + }, + "dependencies": { + "@types/node": { + "version": "12.20.55" + } + } + }, + "@d-fischer/raw-body": { + "version": "2.4.3", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "@d-fischer/shared-utils": { + "version": "3.4.0", + "requires": { + "@types/node": "^14.11.2", + "tslib": "^2.0.3" + } + }, + "@d-fischer/typed-event-emitter": { + "version": "3.3.0", + "requires": { + "@types/node": "^14.11.2", + "tslib": "^2.4.0" + } + }, + "@msgpack/msgpack": { + "version": "2.8.0" + }, + "@serialport/binding-mock": { + "version": "10.2.2", + "optional": true, + "requires": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + } + }, + "@serialport/bindings-cpp": { + "version": "10.8.0", + "optional": true, + "requires": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "^10.2.1", + "debug": "^4.3.2", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.3.0" + } + }, + "@serialport/bindings-interface": { + "version": "1.2.2", + "optional": true + }, + "@serialport/parser-byte-length": { + "version": "10.5.0", + "optional": true + }, + "@serialport/parser-cctalk": { + "version": "10.5.0", + "optional": true + }, + "@serialport/parser-delimiter": { + "version": "10.5.0", + "optional": true + }, + "@serialport/parser-inter-byte-timeout": { + "version": "10.5.0", + "optional": true + }, + "@serialport/parser-packet-length": { + "version": "10.5.0", + "optional": true + }, + "@serialport/parser-readline": { + "version": "10.5.0", + "optional": true, + "requires": { + "@serialport/parser-delimiter": "10.5.0" + } + }, + "@serialport/parser-ready": { + "version": "10.5.0", + "optional": true + }, + "@serialport/parser-regex": { + "version": "10.5.0", + "optional": true + }, + "@serialport/parser-slip-encoder": { + "version": "10.5.0", + "optional": true + }, + "@serialport/parser-spacepacket": { + "version": "10.5.0", + "optional": true + }, + "@serialport/stream": { + "version": "10.5.0", + "optional": true, + "requires": { + "@serialport/bindings-interface": "1.2.2", + "debug": "^4.3.2" + } + }, + "@trycourier/courier": { + "version": "4.1.0", + "requires": { + "cross-fetch": "^3.1.5" + } + }, + "@twurple/api": { + "version": "5.2.7", + "requires": { + "@d-fischer/cache-decorators": "^3.0.0", + "@d-fischer/detect-node": "^3.0.1", + "@d-fischer/logger": "^4.0.0", + "@d-fischer/rate-limiter": "^0.6.1", + "@d-fischer/shared-utils": "^3.2.0", + "@twurple/api-call": "^5.2.7", + "@twurple/common": "^5.2.7", + "tslib": "^2.0.3" + } + }, + "@twurple/api-call": { + "version": "5.2.7", + "requires": { + "@d-fischer/cross-fetch": "^4.0.2", + "@d-fischer/qs": "^7.0.2", + "@twurple/common": "^5.2.7", + "@types/node-fetch": "^2.5.7", + "tslib": "^2.0.3" + } + }, + "@twurple/auth": { + "version": "5.2.7", + "requires": { + "@d-fischer/logger": "^4.0.0", + "@d-fischer/shared-utils": "^3.2.0", + "@twurple/api-call": "^5.2.7", + "@twurple/common": "^5.2.7", + "tslib": "^2.0.3" + } + }, + "@twurple/chat": { + "version": "5.2.7", + "requires": { + "@d-fischer/cache-decorators": "^3.0.0", + "@d-fischer/deprecate": "^2.0.2", + "@d-fischer/logger": "^4.0.0", + "@d-fischer/rate-limiter": "^0.6.1", + "@d-fischer/shared-utils": "^3.2.0", + "@d-fischer/typed-event-emitter": "^3.2.2", + "@twurple/common": "^5.2.7", + "ircv3": "^0.29.7", + "tslib": "^2.0.3" + } + }, + "@twurple/common": { + "version": "5.2.7", + "requires": { + "@d-fischer/shared-utils": "^3.2.0", + "klona": "^2.0.4", + "tslib": "^2.0.3" + } + }, + "@twurple/eventsub": { + "version": "5.2.7", + "requires": { + "@d-fischer/logger": "^4.0.0", + "@d-fischer/raw-body": "^2.4.3", + "@d-fischer/shared-utils": "^3.2.0", + "@d-fischer/typed-event-emitter": "^3.2.2", + "@twurple/auth": "^5.2.7", + "@twurple/common": "^5.2.7", + "@types/express-serve-static-core": "^4.17.24", + "httpanda": "^0.4.6", + "tslib": "^2.0.3" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.31", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/node": { + "version": "14.18.35" + }, + "@types/node-fetch": { + "version": "2.6.2", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "@types/qs": { + "version": "6.9.7" + }, + "@types/range-parser": { + "version": "1.2.4" + }, + "@types/ws": { + "version": "8.5.3", + "requires": { + "@types/node": "*" + } + }, + "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" + } + }, + "array.prototype.map": { + "version": "1.0.5", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.7" + } + }, + "asynckit": { + "version": "0.4.0" + }, + "axios": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "axios-curlirize": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/axios-curlirize/-/axios-curlirize-1.3.7.tgz", + "integrity": "sha512-csSsuMyZj1dv1fL0zRPnDAHWrmlISMvK+wx9WJI/igRVDT4VMgbf2AVenaHghFLfI1nQijXUevYEguYV6u5hjA==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "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.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "optional": true, + "requires": { + "node-gyp-build": "^4.3.0" + } + }, + "bytes": { + "version": "3.1.0" + }, + "call-bind": { + "version": "1.0.2", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==" + }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "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" + } + }, + "combined-stream": { + "version": "1.0.8", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" + }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, + "cross-fetch": { + "version": "3.1.5", + "requires": { + "node-fetch": "2.6.7" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==" + }, + "crypto-js": { + "version": "4.1.1" + }, + "debug": { + "version": "4.3.4", + "requires": { + "ms": "2.1.2" + } + }, + "define-properties": { + "version": "1.1.4", + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "delayed-stream": { + "version": "1.0.0" + }, + "depd": { + "version": "1.1.2" + }, + "detect-node": { + "version": "2.1.0" + }, + "digest-fetch": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-2.0.3.tgz", + "integrity": "sha512-HuTjHQE+wplAR+H8/YGwQjIGR1RQUCEsQcRyp3dZfuuxpSQH4OTm4BkHxyXuzxwmxUrNVzIPf9XkXi8QMJDNwQ==", + "requires": { + "base-64": "^0.1.0", + "js-sha256": "^0.9.0", + "js-sha512": "^0.8.0", + "md5": "^2.3.0" + } + }, + "dotenv": { + "version": "16.0.3" + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "es-abstract": { + "version": "1.20.5", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "unbox-primitive": "^1.0.2" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0" + }, + "es-get-iterator": { + "version": "1.1.2", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.0", + "has-symbols": "^1.0.1", + "is-arguments": "^1.1.0", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "eventemitter3": { + "version": "4.0.7" + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + }, + "form-data": { + "version": "3.0.1", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1" + }, + "function.prototype.name": { + "version": "1.1.5", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3" + }, + "get-intrinsic": { + "version": "1.1.3", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "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", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "has": { + "version": "1.0.3", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3" + }, + "has-tostringtag": { + "version": "1.0.0", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "http-errors": { + "version": "1.7.3", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "httpanda": { + "version": "0.4.6", + "requires": { + "@types/node": "^14.11.2", + "tslib": "^2.0.3" + } + }, + "iconv-lite": { + "version": "0.4.24", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "inherits": { + "version": "2.0.4" + }, + "internal-slot": { + "version": "1.0.4", + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "ircv3": { + "version": "0.29.10", + "requires": { + "@d-fischer/connection": "^7.0.0", + "@d-fischer/escape-string-regexp": "^5.0.0", + "@d-fischer/logger": "^4.0.0", + "@d-fischer/shared-utils": "^3.0.1", + "@d-fischer/typed-event-emitter": "^3.3.0", + "@types/node": "^14.14.19", + "klona": "^2.0.4", + "tslib": "^2.0.3" + } + }, + "is-arguments": { + "version": "1.1.1", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.4", + "requires": { + "has-bigints": "^1.0.1" + } + }, + "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-boolean-object": { + "version": "1.1.2", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-callable": { + "version": "1.2.7" + }, + "is-date-object": { + "version": "1.0.5", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "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-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-map": { + "version": "2.0.2" + }, + "is-negative-zero": { + "version": "2.0.2" + }, + "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-number-object": { + "version": "1.0.7", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-set": { + "version": "2.0.2" + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-weakref": { + "version": "1.0.2", + "requires": { + "call-bind": "^1.0.2" + } + }, + "isarray": { + "version": "2.0.5" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isomorphic-ws": { + "version": "4.0.1", + "requires": {} + }, + "iterate-iterator": { + "version": "1.0.2" + }, + "iterate-value": { + "version": "1.0.2", + "requires": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + } + }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, + "js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==" + }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "klona": { + "version": "2.0.5" + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "long": { + "version": "4.0.0" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "mime-db": { + "version": "1.52.0" + }, + "mime-types": { + "version": "2.1.35", + "requires": { + "mime-db": "1.52.0" + } + }, + "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" + } + }, + "ms": { + "version": "2.1.2" + }, + "node-addon-api": { + "version": "5.1.0", + "optional": true + }, + "node-fetch": { + "version": "2.6.7", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-gyp-build": { + "version": "4.6.0", + "optional": true + }, + "nodemon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.1.tgz", + "integrity": "sha512-k43xGaDtaDIcufn0Fc6fTtsdKSkV/hQzoQFigNH//GaKta28yoKVYXCnV+KXRqfT/YzsFaQU9VdeEG+HEyxr6A==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + } + }, + "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 + }, + "object-inspect": { + "version": "1.12.2" + }, + "object-keys": { + "version": "1.1.1" + }, + "object.assign": { + "version": "4.1.4", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "obs-websocket-js": { + "version": "5.0.2", + "requires": { + "@msgpack/msgpack": "^2.7.1", + "crypto-js": "^4.1.1", + "debug": "^4.3.2", + "eventemitter3": "^4.0.7", + "isomorphic-ws": "^4.0.1", + "type-fest": "^2.3.2", + "ws": "^8.2.2" + } + }, + "obs-websocket-js-27": { + "version": "npm:obs-websocket-js@4.0.3", + "requires": { + "debug": "^4.1.0", + "isomorphic-ws": "^4.0.1", + "sha.js": "^2.4.9", + "ws": "^7.2.0" + }, + "dependencies": { + "utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "optional": true, + "peer": true, + "requires": { + "node-gyp-build": "^4.3.0" + } + }, + "ws": { + "version": "7.5.9", + "requires": {} + } + } + }, + "on-change": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/on-change/-/on-change-4.0.2.tgz", + "integrity": "sha512-cMtCyuJmTx/bg2HCpHo3ZLeF7FZnBOapLqZHr2AlLeJ5Ul0Zu2mUJJz051Fdwu/Et2YW04ZD+TtU+gVy0ACNCA==" + }, + "osc": { + "version": "2.4.4", + "requires": { + "long": "4.0.0", + "serialport": "10.5.0", + "slip": "1.0.2", + "wolfy87-eventemitter": "5.2.9", + "ws": "8.13.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "prettier": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz", + "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==", + "dev": true + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "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" + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.2.1" + }, + "safe-regex-test": { + "version": "1.0.0", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "safer-buffer": { + "version": "2.1.2" + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "serialport": { + "version": "10.5.0", + "optional": true, + "requires": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "10.8.0", + "@serialport/parser-byte-length": "10.5.0", + "@serialport/parser-cctalk": "10.5.0", + "@serialport/parser-delimiter": "10.5.0", + "@serialport/parser-inter-byte-timeout": "10.5.0", + "@serialport/parser-packet-length": "10.5.0", + "@serialport/parser-readline": "10.5.0", + "@serialport/parser-ready": "10.5.0", + "@serialport/parser-regex": "10.5.0", + "@serialport/parser-slip-encoder": "10.5.0", + "@serialport/parser-spacepacket": "10.5.0", + "@serialport/stream": "10.5.0", + "debug": "^4.3.3" + } + }, + "set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "setprototypeof": { + "version": "1.1.1" + }, + "sha.js": { + "version": "2.4.11", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "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==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "slip": { + "version": "1.0.2" + }, + "statuses": { + "version": "1.5.0" + }, + "string.prototype.trimend": { + "version": "1.0.6", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "string.prototype.trimstart": { + "version": "1.0.6", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.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" + } + }, + "toidentifier": { + "version": "1.0.0" + }, + "touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true + }, + "tr46": { + "version": "0.0.3" + }, + "tslib": { + "version": "2.4.1" + }, + "type-fest": { + "version": "2.19.0" + }, + "unbox-primitive": { + "version": "1.0.2", + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "unifi-client": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/unifi-client/-/unifi-client-0.11.0.tgz", + "integrity": "sha512-H1tdd1joGuaFvonw5vOnWgWJkXL93qIr2pfO6UHPJld27F9PO//q7Uz3gbQ6lauEHApswLBG8UPw+QiJ+rGAeg==", + "requires": { + "axios": "1.6.1", + "axios-curlirize": "1.3.7", + "bufferutil": "^4.0.8", + "cookie": "0.6.0", + "debug": "4.3.4", + "jsonwebtoken": "9.0.2", + "semver": "7.5.4", + "set-cookie-parser": "2.6.0", + "utf-8-validate": "^6.0.3", + "ws": "8.14.2" + }, + "dependencies": { + "ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "requires": {} + } + } + }, + "unpipe": { + "version": "1.0.0" + }, + "utf-8-validate": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.3.tgz", + "integrity": "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==", + "optional": true, + "requires": { + "node-gyp-build": "^4.3.0" + } + }, + "webidl-conversions": { + "version": "3.0.1" + }, + "whatwg-url": { + "version": "5.0.0", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "wolfy87-eventemitter": { + "version": "5.2.9" + }, + "ws": { + "version": "8.13.0", + "requires": {} + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..88daab6 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "alveuscontroller", + "version": "1.0.0", + "description": "alveus livestream chat bot", + "main": "src/index.js", + "scripts": { + "prettify": "prettier -l --write \"**/*.js\"", + "dev": "cross-env NODE_ENV=development node src/index.js", + "dev2": "nodemon --inspect=0.0.0.0:9229 -L src/index.js", + "test": "node src/test.js", + "start": "node src/index.js" + }, + "author": "SpaceVoyage", + "license": "UNLICENSED", + "private": true, + "dependencies": { + "@trycourier/courier": "^4.1.0", + "@twurple/api": "^5.2.7", + "@twurple/auth": "^5.2.7", + "@twurple/chat": "^5.2.7", + "@twurple/eventsub": "^5.2.7", + "digest-fetch": "^2.0.1", + "dotenv": "^16.0.3", + "obs-websocket-js": "^5.0.2", + "obs-websocket-js-27": "npm:obs-websocket-js@^4.0.3", + "on-change": "^4.0.2", + "osc": "^2.4.4", + "unifi-client": "^0.11.0" + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true + }, + "devDependencies": { + "cross-env": "^7.0.3", + "prettier": "^3.0.1", + "nodemon": "^3.0.1" + } +} diff --git a/src/.editorconfig b/src/.editorconfig new file mode 100644 index 0000000..fe009fe --- /dev/null +++ b/src/.editorconfig @@ -0,0 +1,5 @@ +[*.js] +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..994301f --- /dev/null +++ b/src/LICENSE @@ -0,0 +1 @@ +Copyright (C) 2024 SpaceVoyage \ No newline at end of file diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..94d5ab4 --- /dev/null +++ b/src/README.md @@ -0,0 +1,14 @@ +# alveuscontroller + +Alveus node app to control the livestream. + +Connections: +- OBS +- Axis Cameras +- OBSBot Camera +- Twitch +- Unifi +- Courier + +Setup: +Create a .env file with connection information \ No newline at end of file diff --git a/src/config/config.js b/src/config/config.js new file mode 100644 index 0000000..b41452d --- /dev/null +++ b/src/config/config.js @@ -0,0 +1,1212 @@ +//---------- Config ----------------------------------------------- +const devPrefix = process.env.NODE_ENV == 'development' ? `$` : '!'; +const commandPrefix = devPrefix; + +const twitchChannelList = ["spacevoyage", "alveussanctuary", "alveusgg"]; + +const alveusTwitchID = 636587384; +const pauseNotify = true; +const pauseGameChange = true; +const pauseTwitchMarker = true; +const pauseCloudSceneChange = false; +const announceChatSceneChange = false; +//UTC TIME +const notifyHours = {start:13,end:23}; +const restrictedHours = {start:13,end:23}; +const globalMusicSource = "Music Playlist Global"; + +let userPermissions = { + commandPriority: ["commandAdmins", "commandSuperUsers", "commandMods", "commandOperator", "commandVips", "commandUsers"], + commandAdmins: ["spacevoyage", "maya", "theconnorobrien", "alveussanctuary"], + commandSuperUsers: ["geologyrocks01", "dionysus1911", "dannyDV", "maxzillaJr", "illjx", "kayla_alveus", "alex_b_patrick", "lindsay_alveus", "strickknine","tarantulizer","SpiderdayNightLive"], + commandMods: ["mods"], + commandOperator: ["96allskills", "stolenarmy_", "berlac", "knayte_", "dansza", "loganrx_", "merger3", "nitelitedf", "purplemartinconservation","wazix11","lazygoosepxls","alxiszzz"], + commandVips: ["vips", "tfries_", "sivvii_", "ghandii_", "axialmars", "wolfone_", "dohregard", "shutupleonard", + "jazz_peru", "stealfydoge", "xano218", "experimentalcyborg", "klav___", "monkarooo","nixxform","MadCharlieKelly", + "josh_raiden", "jateu", "storesE6", "rebecca_h9", "matthewde", "lakel1", "user_11_11", "huniebeeXD","kurtyykins", + "breacherman", "bryceisrightjr","sumaxu","mariemellie","darkrow_","ewok_626","quokka64","lumberaxe1","minipurrl","taizun"], + commandUsers: ["subs"] +} + +let userBlacklist = ["RestreamBot"]; + +//OBS Scene Commands +const commandPermissionsScenes = { + commandAdmins: ["testadminscene"], + commandSuperUsers: ["testsuperscene", "backpackcam", "localbackpackcam", "serverpccam", "localpccam", "servernuthousecam", "phonecam"], + commandMods: ["testmodscene", "alveusserver", "brbscreen", "georgiecambackup", "noodlecambackup", "hankcambackup", "hankcam2backup", "roachcambackup", "isopodcambackup", + "noodlegeorgiecambackup", "georgienoodlecambackup", "3cambackup", "4cambackup", "ellaintro", "kaylaintro", "connorintro","intro","poboxintro","aqintro", + "noodlehidecambackup", "georgiewatercambackup", "parrotcambackup", "pasturecambackup", "crowcambackup", "crowcam2backup", "crowcam3backup", + "foxcambackup", "foxcam2backup", "foxcam3backup", "foxcam4backup", + "4camoutdoorbackup", "marmosetcambackup", "marmosetcam2backup", "marmosetcam3backup"], + commandOperator: [], + commandVips: [], + commandUsers: [] +} + +let timeRestrictedCommands = ["parrotcam", "pasturecam", "crowcam", "crowcam2", "crowcam3", "foxcam", "foxcam2", "foxcam3", "foxcam4", "4camoutdoor", + "marmosetcam", "marmosetcam2", "marmosetcam3", + "parrotcambackup", "pasturecambackup", "crowcambackup", "crowcam2backup", "crowcam3backup", + "foxcambackup", "foxcam2backup", "foxcam3backup", "foxcam4backup", "4camoutdoorbackup", + "marmosetcambackup", "marmosetcam2backup", "marmosetcam3backup"]; + + +let timeRestrictedScenes = ["parrot", "fox", "foxcorner", "foxmulti", "pasture", "crow", "crowmulti", "crowoutdoor", "marmoset", "marmosetindoor", "marmosetmulti"]; +let throttledCommands = []; + +const throttleCommandLength = 30000; + +//Customcam scene names +const commandPermissionsCustomCam = { + commandAdmins: [], + commandSuperUsers: ["nuthousecam", "localpccam", "backpackcam", "phonecam"], + commandMods: ["wolfcam","wolfcam2","wolfcam3","wolfcam4","wolfcam5","wolfcam6","parrotcam", "pasturecam", "crowcam", "crowcam2", "crowcam3", + "foxcam", "foxcam2", "foxcam3", "foxcam4", "4camoutdoor", "marmosetcam", "marmosetcam2", "marmosetcam3", + "nightcams", "nightcamsbig","chickencam"], + commandOperator: ["constructioncam"], + commandVips: ["georgiecam", "noodlecam", "puppycam", "hankcam", "hankcam2", "hankcam3", "hankmulti", "roachcam", "isopodcam", + "noodlehidecam", "georgiewatercam", "georgiemulticam", "indoorcams", "indoorcamsbig", "chincam", "ratcam","ratcam2","ratcam3","ratcam4","orangeisopodcam"], + commandUsers: [] +} + + +//customcam lowercase, no spaces, no s/es +const multiCustomCamScenes = { + wolf: ["wolf", "wolfcorner","wolfindoor","wolfden","wolfden2","wolfmulti"], + fox: ["fox", "foxcorner", "foxmulti", "foxden", "foxmulti2", "foxmulti3"], + crow: ["crow", "crowmulti", "crowoutdoor"], + marmoset: ["marmoset", "marmosetindoor", "marmosetmulti", "marmosetmulti"], + // rat: ["rat","rat2","rat3","ratmulti"] +} + +//One Direction, If on OBS Scene, allow subscenes commands +//scene names are lowercase, no spaces, no s/es +let onewayCommands = { + "4camoutdoor": ["foxcam", "foxcam2", "foxcam3", "foxcam4", "pasturecam"] +} + +//Chat Command Swapping +//key names are same as multiscenes +//links command to obs scene names +let multiCommands = { + crow: ["crowcam", "crowcam2", "crowcam3"], + fox: ["foxcam", "foxcam2", "foxcam3", "foxcam4"], + wolf: ["wolfcam", "wolfcam2","wolfcam3","wolfcam4","wolfcam5","wolfcam6"], + marmoset: ["marmosetcam", "marmosetcam2", "marmosetcam3"], + // rat: ["ratcam","ratcam2","ratcam3","ratcam4","ratcamall"] +} + +//Notification Swapping +//Scene Names in OBS +//lowercase, no spaces, no s/es +const multiScenes = { + crow: ["crow", "crowoutdoor", "crowmulticam"], + wolf: ["wolf", "wolfcorner","wolfindoor","wolfden","wolfden2","wolfmulti"], + fox: ["fox", "foxden", "foxmulticam", "foxcorner"], + marmoset: ["marmoset", "marmosetindoor", "marmosetmulti"], + // rat: ["ratcam","ratcam2","ratcam3","ratcam4"] +} + +const onewayNotifications = { + "4camoutdoor": ["foxes", "fox den", "fox multicam", "fox corner", "pasture"] +} + +//Scene Names in OBS +const notifyScenes = ["Parrots", "Parrots Muted Mic", "Crows", "Crows Outdoor", "Crows Muted Mic", + "Crows Multicam", "Nuthouse", "4 Cam Crows", "3 Cam Crows", "Pasture", "Foxes", + "Marmoset", "Marmoset Indoor", "Marmoset Multi", + "Fox Den", "Fox Corner", "Fox Multicam", "Fox Muted Mic", "4 Cam Outdoor"]; + + +//Audio source in OBS +//lowercase, no spaces, no s/es, cleanName() +const sceneAudioSource = { + "music": globalMusicSource, + "pasture": "Pasture Camera", + "fox": "fox mic", + "foxden": "fox mic", + "foxcorner": "fox mic", + "foxmulticam": "fox mic", + "parrot": "Parrot Camera", + "crow": "crow mic", + "crowoutdoor": "crow mic", + "crowindoor": "crow mic", + "crowmulticam": "crow mic", + "marm": "marmoset mic", + "marmoset": "marmoset mic", + "marmosetindoor": "marmoset mic", + "marmosetoutdoor": "marmoset mic", + "marmosetmulti": "marmoset mic", + "nuthouse": "nuthouse local", + "nut": "nuthouse local", + "phone": "alveus rtmp mobile", + "backpack": "maya rtmp 1", + "pc": "local rtmp desktop", + "wolf": "wolf mic", + "wolfcorner": "wolf mic", + "wolfindoor": "wolf mic", + "wolfden": "wolf den2 camera", + "wolfden2": "wolf den2 camera", + "wolfmulti": "wolf mic", + "chatchat": "chat chats audio", + "phonemic": "alveus rtmp mobile mic", + "chicken": "Chicken Camera" +} +//used for unmute/mute all. match above phrasing +const micGroups = { + livecams: { + pasture: { name: sceneAudioSource.pasture, volume: -2.4 }, parrot: { name: sceneAudioSource.parrot, volume: -7.9 }, + crow: { name: sceneAudioSource.crow, volume: -7.6 }, marmoset: { name: sceneAudioSource.marm, volume: -7.6 }, + wolf: { name: sceneAudioSource.wolf, volume: -7.9 }, wolfden: { name: sceneAudioSource.wolfden2, volume: -7.9 }, + chicken: { name: sceneAudioSource.chicken, volume: -7.9 } + }, + restrictedcams: { + fox: { name: sceneAudioSource.fox, volume: -2.4 } + }, + admincams: { + phone: { name: sceneAudioSource.phone, volume: 0 }, + backpack: { name: sceneAudioSource.backpack, volume: 0 }, pc: { name: sceneAudioSource.pc, volume: 0 }, + nuthouse: { name: sceneAudioSource.nut, volume: 0 }, + chatchat: { name: sceneAudioSource.chatchat, volume: 0 }, + phonemic: { name: sceneAudioSource.phonemic, volume: 0 } + } +} + + + + +//ADD IP INFO IN ENV +//Scene Names in OBS +//lowercase, no spaces, no s/es +const axisCameras = ["pasture", "parrot","wolf","wolfindoor","wolfcorner","wolfden2","wolfden","georgie", "georgiewater", "noodle", "roach", "crow", "crowoutdoor", "fox", "foxden", + "foxcorner", "hank", "hankcorner", "marmoset", "marmosetindoor", "chin", "puppy", "marty", "bb","construction","chicken"]; + +//Axis Camera Mapping to Command. Converting base to source name +const axisCameraCommandMapping = { + "pasture":"pasture", + "parrot":"parrot", + "wolf":"wolf", + "wolfcam2":"wolfcorner", + "wolfcam3":"wolfden", + "wolfcam4":"wolfden2", + "wolfcam5":"wolfindoor", + "wolfcam6":"wolfmulti", + "georgie":"georgie", + "georgiewater":"georgiewater", + "noodle":"noodle", + "roach":"roach", + "crow":"crow", + "crowcam2":"crowoutdoor", + "fox":"fox", + "foxcam4":"foxden", + "foxcam3":"foxcorner", + "hank":"hank", + "hankcam2":"hankcorner", + "marmoset":"marmoset", + "marmosetcam2":"marmosetindoor", + "chin":"chin", + "puppy":"puppy", + "isopod":"marty", + "orangeisopod":"bb", + "construction":"construction", + "chickencam":"chicken" + // "ratcam":"rat" +} + +//Camera Commands +const ptzPrefix = "ptz"; +const commandPermissionsCamera = { + commandAdmins: ["testadmincamera"], + commandSuperUsers: ["testsupercamera", "ptzcontrol", "ptzoverride", "ptzclear"], + commandMods: ["testmodcamera", "ptztracking", "ptzspeed", "ptzspin", "ptzirlight", "ptzwake"], + commandOperator: ["ptzhomeold","ptzseta","ptzgetinfo","ptzset", "ptzpan", "ptztilt", "ptzmove", "ptzir", "ptzdry", "ptzfov", "ptzstop", "ptzsave", "ptzremove", "ptzrename"], + commandVips: ["ptzhome", "ptzpreset", "ptzzoom", "ptzload", "ptzlist", "ptzroam", "ptzroaminfo", "ptzfocus", "ptzgetfocus", "ptzfocusr", "ptzautofocus"], + commandUsers: [] +} +//timeRestrictedCommands = timeRestrictedCommands.concat(["ptzclear"]); +//throttledCommands = throttledCommands.concat([]); + +//Extra Commands +const commandPermissionsExtra = { + commandAdmins: ["testadminextra"], + commandSuperUsers: ["testsuperextra", "resetcloudsource", "resetcloudsourcef", "setalveusscene", "setcloudscene", "changeserver", "setmute", "camclear"], + commandMods: ["testmodextra", "resetsource","resetsourcef","camload", "camlist", "camsave", "camrename", "campresetremove", "customcams", "customcamsbig", "customcamstl", "customcamstr", "customcamsbl", "customcamsbr", + "unmutecam", "unmuteallcams", "nightcams", "nightcamsbig", "indoorcams", "addcam"], + commandOperator: [], + commandVips: ["getvolume", "setvolume", "resetvolume", "removecam","swapcam", "mutecam", "muteallcams", "musicvolume", "musicnext", "musicprev", "mutemusic", "unmutemusic", "mutemusiclocal", "unmutemusiclocal", "resetbackpack", "resetpc", "resetlivecam", "resetbackpackf", "resetpcf", "resetlivecamf", "resetcam", "resetextra","resetphone", "resetphonef"], + commandUsers: [] +} +timeRestrictedCommands = timeRestrictedCommands.concat(["unmutecam", "unmuteallcams"]); +throttledCommands = throttledCommands.concat(["swapcam", "mutemusic", "unmutemusic", "mutemusiclocal", "unmutemusiclocal", "resetbackpack", "resetpc", "resetlivecam", "resetbackpackf", "resetpcf", "resetlivecamf", "resetcam", "resetphone", "resetphonef","resetextra"]); + +//Unifi +const commandPermissionsUnifi = { + commandAdmins: [], + commandSuperUsers: ["apclientinfo", "apclientreconnect"], + commandMods: ["apsignal", "apreconnect"], + commandOperator: [], + commandVips: [], + commandUsers: [] +} + +//CCam Argument for Command Mapping. Converting base to source name +const customCamCommandMapping = { + "hankcam2": "hankcorner", + "hankcam3": "hankmulti", + "noodlehidecam": "noodlehide", + "georgiewatercam": "georgiewater", + "georgiemulticam": "georgiemulti", + "crowcam": "crow", + "crowcam2": "crowoutdoor", + "crowcam3": "crowmulti", + "foxcam": "fox", + "foxcam2": "foxmulti", + "foxcam3": "foxcorner", + "foxcam4": "foxden", + "marmosetcam": "marmoset", + "marmosetcam2": "marmosetindoor", + "marmosetcam3": "marmosetmulti", + "3cam": "georgie noodle isopod", + "4cam": "georgie noodle isopod roach", + "4camoutdoor": "pasture parrot marmoset fox", + "nightcams": "wolf pasture parrot fox crow marmoset", + "nightcamsbig": "wolf pasture parrot fox crow marmoset", + "indoorcams": "georgie hank puppy chin isopod roach", + "indoorcamsbig": "georgie hank puppy chin isopod roach", + "chincam": "chin", + "ratcam": "rat", + "ratcam2": "rat2", + "ratcam3": "rat3", + "ratcam4": "ratmulti", + "ratcamall": "rat rat2 rat3", + "localpccam": "pc", + "orangeisopodcam": "orangeisopod", + "roachcam":"roaches", + "constructioncam":"construction", + "wolfcam2":"wolfcorner", + "wolfcam3":"wolfden", + "wolfcam4":"wolfden2", + "wolfcam5":"wolfindoor", + "wolfcam6":"wolfmulti", +} + +const commandSceneAlias = { + localpccam: ["desktopcam","pclocalcam","pccamlocal","alveuspccam"], + serverpccam: ["pccam","pcservercam", "remotepccam","serverpccam"], + phonecam: ["alveusphonecam", "winniecam", "goatcam"], + puppycam: ["scorpioncam"], + roachcam: ["roachescam","barbaracam"], + hankcam: ["mrmctraincam ", "choochoocam", "hankthetankchoochoomrmctraincam"], + hankcam2: ["mrmctraincam2 ", "choochoocam2", "hankthetankchoochoomrmctraincam3", "hanknightcam", "hankcornercam"], + hankcam3: ["mrmctraincam3 ", "choochoocam3", "hankthetankchoochoomrmctraincam3", "hankmulticam"], + isopodcam: ["isopodscam", "martycam", "martyisopodcam"], + orangeisopodcam: ["bbcam", "bbisopodcam", "sisopodcam", "isopodorangecam", "oisopodcam", "spanishisopodcam", "isopod2cam", "isopodcam2"], + georgiecam: ["georgcam"], + georgiewatercam: ["georgieunderwatercam"], + nuthousecambackup: ["nutcam"], + servernuthousecam: ["servernutcam", "remotenutcam", "remotenuthousecam"], + crowcam: ["crowcamindoor", "crowindoorcam","crowincam"], + crowcam2: ["crowcamoutdoor", "crowcamoutdoors", "crowoutdoorcam","crowoutcam"], + crowcam3: ["crowcammulti", "crowmulticam"], + foxcam: ["foxcam", "foxescam"], + foxcam2: ["foxmulticam", "foxcammulti"], + foxcam3: ["foxwideangle", "foxcornercam"], + foxcam4: ["foxdencam", "foxcamden"], + marmosetcam: ["marmosetoutdoorcam", "marmosetscam", "marmcam", "marmscam", "marmsoutdoorcam", "marmsoutcam", "marmoutdoorcam", "marmoutcam"], + marmosetcam2: ["marmosetindoorcam", "marmosetsindoorcam", "marmsindoorcam", "marmsincam", "marmindoorcam", "marmincam"], + marmosetcam3: ["marmosetmulticam", "marmosetsmulticam", "marmsmulticam", "marmmulticam"], + "4camoutdoor": ["4camoutdoors", "multioutdoor"], + nightcams: ["nightcam", "outdoorcams", "outdoorcam", "outsidecam", "outsidecams", "livecams", "livecam"], + nightcamsbig: ["nightcambig", "outdoorcamsbig", "outdoorcambig", "outsidecambig", "outsidecamsbig", "nightcamb", "nightcams2", "ncb"], + indoorcams: ["4cams", "indoorcam", "insidecams", "insidecam"], + indoorcamsbig: ["indoorcambig", "insidecamsbig", "insidecambig"], + chincam: ["chinchillacam", "chinscam", "chincam", "snorkcam", "moomincam", "fluffycam"], + ratcam: ["ratcam1","rattopcam", "rat1cam","nillacam","chipscam","chipcam","rattcam"], + ratcam2: ["ratmiddlecam", "rat2cam","ratmcam"], + ratcam3: ["ratbottomcam", "rat3cam","ratbcam"], + ratcam4: ["ratmulticam", "rat4cam"], + ratcamall: ["ratallcam", "ratstackcam"], + connorpc: ["connordesktop"], + constructioncam: ["timelapsecam"], + connorintro: ["penis"], + chatchat: ["bugmic","chatchatmic"], + phonemic: ["phoneaudio","phonemic","mobilemic"], + wolfcam: ["wolvescam","timbercam","awacam"], + wolfcam2: ["wolvescornercam","wolfcornercam"], + wolfcam3: ["wolvesdencam","wolfdencam"], + wolfcam4: ["wolvesden2cam","wolfden2cam"], + wolfcam5: ["wolvesindoorcam","wolfindoorcam"], + wolfcam6: ["wolvesmulticam","wolfmulticam"], +} + +const commandControlAlias = { + ptzfocus: ["ptzsetfocus"], + ptzfocusr: ["ptzsetfocusr"], + ptzdry: ["ptzshake", "ptzhecrazy"], + "resetlivecam": ["resetlivecams", "restartlivecam", "restartlivecams"], + "resetlivecamf": ["resetlivecamsf", "restartlivecamf", "restartlivecamsf"], + "resetbackpack": ["resetbackpackcam", "restartbackpack", "restartbackpackcam"], + "resetbackpackf": ["resetbackpackcamf", "restartbackpackf", "restartbackpackcamf"], + "resetextra": ["resetextracam"], + "resetphone": ["resetphonecam"], + "resetphonef": ["resetphonecamf"], + customcams: ["cc", "ccams", "ccam", "customcam"], + customcamsbig: ["ccb", "ccamsb", "ccamb", "customcambig", "customcamb", "customcamsb"], + customcamstl: ["piptl", "customcamtl", "customcamtopleft", "pipul"], + customcamstr: ["piptr", "customcamtr", "customcamtopright", "pipur"], + customcamsbl: ["pipbl", "customcambl", "customcambottomleft", "pipll"], + customcamsbr: ["pipbr", "customcambr", "customcambottomright", "piplr"], + camsave: ["camssave", "savelayout", "layoutsave", "savecam"], + camload: ["camsload", "loadcam", "loadcams", "loadlayout", "loadpreset"], + campresetremove: ["campresetremove", "removecampreset", "removelayout", "removepreset"], + camrename: ["camsrename", "renamecam", "renamecams"], + camlist: ["camslist", "listcam", "listcams", "listlayout", "listlayouts", "layouts"], + setvolume: ["volumeset", "camvolume", "micvolume"], + getvolume: ["volumeinfo", "volumeget","getvolumes","volume"], + resetvolume: ["volumereset", "camvolumereset", "micvolumereset", "resetvolumes", "resetmic", "resetmics"], + unmutecam: ["unmute", "unmutemic"], + unmuteallcams: ["unmuteall", "unmutecamsall", "unmutecamall", "unmuteallmic"], + mutecam: ["mute", "mutemic"], + muteallcams: ["muteall", "mutecamsall", "mutecamall", "muteallmic"], + mutemusic: ["musicoff", "musicmute"], + unmutemusic: ["musicon", "musicunmute"], + mutemusiclocal: ["mutemusicl", "musicoffl"], + unmutemusiclocal: ["unmutemusicl", "musicunmutel"], + musicvolume: ["setmusicvolume", "changemusicvolume"], + musicnext: ["nextmusic", "nextsong", "musicforward"], + musicprev: ["prevmusic", "prevsong", "previousmusic", "previoussong", "lastsong", "musicback"], + removecam: ["removecams", "remove", "hidecam", "hide"], + addcam: ["addcams", "add", "showcam", "show"], + swapcam: ["movecam", "swapcams", "movecams", "swap"], + setalveusscene: ["setscene", "changescene", "changealveusscene"], + setcloudscene: ["changecloudscene"], + apsignal: ["apinfo", "liveu", "liveusignal", "liveuinfo", "liveustatus", "signal", "wifi"], + apreconnect: ["apreset", "liveureset", "liveureconnect", "resetliveu", "reconnectliveu"], + ptzgetfocus: ["getfocus"], +} + +let commandScenes = { + backpackcam: "Backpack Server", //Cloud server + serverpccam: "Alveus PC Server",//"Alveus PC Server", //Cloud server + phonecam: "Phone Server", //Cloud server + servernuthousecam: "fullcam nuthouse", + brbscreen: "BRB", //Cloud server + ellaintro: "Ella Intro", + kaylaintro: "Kayla Intro", + connorintro: "Connor Intro", + poboxintro: "POBox Intro", + aqintro: "AQIntro", + intro: "INTRO", + localbackpackcam: "Backpack", + localpccam: "Alveus PC", + nuthousecambackup: "fullcam nuthouse", + parrotcambackup: "fullcam parrot", + pasturecambackup: "fullcam pasture", + georgiecambackup: "fullcam georgie", + noodlecambackup: "fullcam noodle", + hankcambackup: "fullcam hank", + hankcam2backup: "fullcam hankcorner", + roachcambackup: "fullcam roach", + isopodcambackup: "fullcam orangeisopod", + noodlegeorgiecambackup: "Noodle /Georgie", + georgienoodlecambackup: "Georgie / Noodle", + "3cambackup": "3 Cam", + "4cambackup": "4 Cam", + noodlehidecambackup: "Noodle Hide", + georgiewatercambackup: "fullcam georgiewater", + crowcambackup: "fullcam crow", + crowcam2backup: "fullcam crowoutdoor", + crowcam3backup: "fullcam crowmulti", + marmosetcambackup: "fullcam marmoset", + marmosetcam2backup: "fullcam marmosetindoor", + marmosetcam3backup: "fullcam marmosetmulti", + foxcambackup: "fullcam fox", + foxcam2backup: "fullcam foxcorner", + foxcam3backup: "fullcam foxmulti", + foxcam4backup: "fullcam foxmulti2", + "4camoutdoorbackup": "4 Cam Outdoor", +} + +let commandScenesCloud = { + backpackcam: "Maya LiveU", + serverpccam: "Alveus PC", + phonecam: "Phone", + brbscreen: "BRB", + servernuthousecam: "Alveus Nuthouse", + ellaintro: "Ella Intro", + kaylaintro: "Kayla Intro", + connorintro: "Connor Intro", + poboxintro: "POBox Intro", + aqintro: "AQIntro", + intro: "Intro", + localbackpackcam: "Alveus Server", + localpccam: "Alveus Server", + parrotcambackup: "Alveus Server", + pasturecambackup: "Alveus Server", + nuthousecambackup: "Alveus Server", + georgiecambackup: "Alveus Server", + noodlecambackup: "Alveus Server", + hankcambackup: "Alveus Server", + hankcam2backup: "Alveus Server", + roachcambackup: "Alveus Server", + isopodcambackup: "Alveus Server", + noodlegeorgiecambackup: "Alveus Server", + georgienoodlecambackup: "Alveus Server", + "3cambackup": "Alveus Server", + "4cambackup": "Alveus Server", + noodlehidecambackup: "Alveus Server", + georgiewatercambackup: "Alveus Server", + alveusserver: "Alveus Server", + crowcambackup: "Alveus Server", + crowcam2backup: "Alveus Server", + crowcam3backup: "Alveus Server", + marmosetcambackup: "Alveus Server", + marmosetcam2backup: "Alveus Server", + marmosetcam3backup: "Alveus Server", + foxcambackup: "Alveus Server", + foxcam2backup: "Alveus Server", + foxcam3backup: "Alveus Server", + foxcam4backup: "Alveus Server", + "4camoutdoorbackup": "Alveus Server" +} + + + +//----------------------------------------------------------- + +const commandAlias = { ...commandControlAlias, ...commandSceneAlias }; + +//create Permission List +const commandPermissions = getCommandPermissions(); +//create Command List +const commandList = getCommandList(commandPermissions, commandAlias); +commandScenes = setupCommandScenes(commandScenes); +commandScenesCloud = setupCommandScenes(commandScenesCloud); +multiCommands = setupCommandAliasMap(multiCommands); +onewayCommands = setupCommandAliasMap(onewayCommands); +timeRestrictedCommands = setupCommandAliasArray(timeRestrictedCommands); +throttledCommands = setupCommandAliasArray(throttledCommands); + +const commandAliasConverted = setupCommandAliasConversion(commandAlias); +const multiCustomCamScenesConverted = setupCommandAliasConversion(multiCustomCamScenes); +const customCommandAlias = setupCustomCamAlias(commandSceneAlias); +const customSceneCommands = getCommandList(commandPermissionsCustomCam, commandSceneAlias); + +userBlacklist.forEach(user => { + user = user.toLowerCase().trim(); +}); + +for (const permission of userPermissions.commandPriority) { + userPermissions[permission].forEach(user => { + user = user.toLowerCase().trim(); + }); +} + +function getCommandList(commandObj, aliasObj) { + //add Alias commands to user permissions + for (const parentCommand in aliasObj) { + //check Alias names + for (const permission in commandObj) { + //find matching permission location + if (commandObj[permission].includes(parentCommand)) { + //add all alias's + for (const alias of aliasObj[parentCommand]) { + commandObj[permission].push(alias); + } + } + } + } + //get full list of all possible commands + let list = getListOfCommands(commandObj); + return list +} + +function getCommandPermissions() { + const commandPermissions = {}; + //get full list of all possible commands + for (const permission of userPermissions.commandPriority) { + let scenes = commandPermissionsScenes[permission] || []; + let customCam = commandPermissionsCustomCam[permission] || []; + let camera = commandPermissionsCamera[permission] || []; + let extra = commandPermissionsExtra[permission] || []; + let unifi = commandPermissionsUnifi[permission] || []; + commandPermissions[permission] = [].concat(scenes, customCam, camera, extra, unifi); + } + return commandPermissions; +} + +function getListOfCommands(commandObj) { + const list = []; + //get full list of all possible commands + for (const permission in commandObj) { + for (const command of commandObj[permission]) { + let c = command || "" + c = c.toLowerCase(); + if (c != "" && !list.includes(c.toLowerCase())) { + list.push(c.toLowerCase()); + } + } + } + return list; +} + +function setupCommandScenes(sceneMap) { + //add alias commands to commandScenes + for (const parentCommand in commandAlias) { + //get scene for parent command + const scene = sceneMap[parentCommand]; + //scene command, not extra + if (scene != null) { + //add each alias + for (const alias of commandAlias[parentCommand]) { + sceneMap[alias] = scene; + } + } + } + return sceneMap; +} + +function setupCommandAliasMap(commandList) { + for (const baseCommand in commandList) { + for (const mainCommand of commandList[baseCommand]) { + //get every command from Multicommand + const aliasList = commandAlias[mainCommand]; + if (aliasList != null) { + //check if alias commands for the maincommand + for (let i = 0; i < aliasList.length; i++) { + const alias = aliasList[i]; + //add all to multicommand list + if (!commandList[baseCommand].includes(alias)) { + commandList[baseCommand].push(alias); + } + } + } + } + } + return commandList; +} + +function setupCommandAliasArray(commandList) { + for (let i = 0; i < commandList.length; i++) { + //get every command from Multicommand + const mainCommand = commandList[i]; + const aliasList = commandAlias[mainCommand]; + if (aliasList != null) { + //check if alias commands for the maincommand + for (let j = 0; j < aliasList.length; j++) { + const alias = aliasList[j]; + //add all to multicommand list + if (!commandList.includes(alias)) { + commandList.push(alias); + } + } + } + } + return commandList; +} + +function setupCommandAliasConversion(aliasList) { + const convertedList = {}; + for (const baseCommand in aliasList) { + for (const aliasCommand of aliasList[baseCommand]) { + convertedList[aliasCommand] = baseCommand; + } + } + return convertedList; +} + +function setupCustomCamAlias(aliasList) { + const convertedList = {}; + for (const baseCommand in aliasList) { + + let newBaseCommand = baseCommand.toLowerCase(); + newBaseCommand = newBaseCommand.replaceAll(/e?s(\s|\W|$|multi(?:cam)?|cam|outdoor|indoor|wideangle|corner|den)/g, "$1"); + newBaseCommand = newBaseCommand.replaceAll(/(?:full)?cams?/g, ""); + // newBaseCommand = newBaseCommand.replaceAll(" ", ""); + + let convertedBaseCommand = customCamCommandMapping[baseCommand] || baseCommand; + + let newConvertedBaseCommand = convertedBaseCommand.toLowerCase(); + newConvertedBaseCommand = newConvertedBaseCommand.replaceAll(/e?s(\s|\W|$|multi(?:cam)?|cam|outdoor|indoor|wideangle|corner|den)/g, "$1"); + newConvertedBaseCommand = newConvertedBaseCommand.replaceAll(/(?:full)?cams?/g, ""); + + + convertedList[newBaseCommand] = newConvertedBaseCommand; + + for (const aliasCommand of aliasList[baseCommand]) { + let newAliasCommand = aliasCommand.toLowerCase(); + newAliasCommand = newAliasCommand.replaceAll(/e?s(\s|\W|$|multi(?:cam)?|cam|outdoor|indoor|wideangle|corner|den)/g, "$1"); + newAliasCommand = newAliasCommand.replaceAll(/(?:full)?cams?/g, ""); + newAliasCommand = newAliasCommand.replaceAll(" ", ""); + if (!isNaN(parseInt(newAliasCommand))) { + newAliasCommand = newAliasCommand + "cam"; + } + convertedList[newAliasCommand] = newConvertedBaseCommand; + } + } + return convertedList; +} + +const scenePositions = { + "1box": { + 1: { //fullscreen + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 1920, + positionX: 0, + positionY: 0, + rotation: 0, + scaleX: 1, + scaleY: 1, + sourceHeight: 1080, + sourceWidth: 1920, + width: 1080 + } + }, + "2box": { + 1: { //middle left + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 540, + positionX: 0, + positionY: 270, + rotation: 0, + scaleX: 0.5, + scaleY: 0.5, + sourceHeight: 1080, + sourceWidth: 1920, + width: 960 + }, + 2: { //middle right + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 540, + positionX: 960, + positionY: 270, + rotation: 0, + scaleX: 0.5, + scaleY: 0.5, + sourceHeight: 1080, + sourceWidth: 1920, + width: 960 + } + }, + "2boxbig": { + 1: { //big + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 720, + positionX: 640, + positionY: 180, + rotation: 0, + scaleX: 0.66666, + scaleY: 0.66666, + sourceHeight: 1080, + sourceWidth: 1920, + width: 1280 + }, + 2: { //middle left + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 0, + positionY: 360, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + } + }, + "2boxtl": { + 1: { //fullscreen + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 1920, + positionX: 0, + positionY: 0, + rotation: 0, + scaleX: 1, + scaleY: 1, + sourceHeight: 1080, + sourceWidth: 1920, + width: 1080 + }, + 2: { //topleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 0, + positionY: 0, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + } + }, + "2boxtr": { + 1: { //fullscreen + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 1920, + positionX: 0, + positionY: 0, + rotation: 0, + scaleX: 1, + scaleY: 1, + sourceHeight: 1080, + sourceWidth: 1920, + width: 1080 + }, + 2: { //topright + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 1280, + positionY: 0, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + } + }, + "2boxbl": { + 1: { //fullscreen + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 1920, + positionX: 0, + positionY: 0, + rotation: 0, + scaleX: 1, + scaleY: 1, + sourceHeight: 1080, + sourceWidth: 1920, + width: 1080 + }, + 2: { //topright + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 0, + positionY: 720, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + } + }, + "2boxbr": { + 1: { //fullscreen + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 1920, + positionX: 0, + positionY: 0, + rotation: 0, + scaleX: 1, + scaleY: 1, + sourceHeight: 1080, + sourceWidth: 1920, + width: 1080 + }, + 2: { //bottomright + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 1280, + positionY: 720, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + } + }, + "3box": { + 1: { //topcenter + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 540, + positionX: 480, + positionY: 0, + rotation: 0, + scaleX: 0.5, + scaleY: 0.5, + sourceHeight: 1080, + sourceWidth: 1920, + width: 960 + }, + 2: { //bottomleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 540, + positionX: 0, + positionY: 540, + rotation: 0, + scaleX: 0.5, + scaleY: 0.5, + sourceHeight: 1080, + sourceWidth: 1920, + width: 960 + }, + 3: { //bottomright + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 540, + positionX: 960, + positionY: 540, + rotation: 0, + scaleX: 0.5, + scaleY: 0.5, + sourceHeight: 1080, + sourceWidth: 1920, + width: 960 + } + }, + "3boxbig": { + 1: { //big + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 720, + positionX: 640, + positionY: 180, + rotation: 0, + scaleX: 0.66666, + scaleY: 0.66666, + sourceHeight: 1080, + sourceWidth: 1920, + width: 1280 + }, + 2: { //topleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 0, + positionY: 180, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + }, + 3: { //bottomleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 0, + positionY: 540, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + } + }, + "4box": { + 1: { //topleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 540, + positionX: 0, + positionY: 0, + rotation: 0, + scaleX: 0.5, + scaleY: 0.5, + sourceHeight: 1080, + sourceWidth: 1920, + width: 960 + }, + 2: { //topright + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 540, + positionX: 960, + positionY: 0, + rotation: 0, + scaleX: 0.5, + scaleY: 0.5, + sourceHeight: 1080, + sourceWidth: 1920, + width: 960 + }, + 3: { //bottomleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 540, + positionX: 0, + positionY: 540, + rotation: 0, + scaleX: 0.5, + scaleY: 0.5, + sourceHeight: 1080, + sourceWidth: 1920, + width: 960 + }, + 4: { //bottomright + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 540, + positionX: 960, + positionY: 540, + rotation: 0, + scaleX: 0.5, + scaleY: 0.5, + sourceHeight: 1080, + sourceWidth: 1920, + width: 960 + } + }, + "4boxbig": { + 1: { //big + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 720, + positionX: 640, + positionY: 180, + rotation: 0, + scaleX: 0.66666, + scaleY: 0.66666, + sourceHeight: 1080, + sourceWidth: 1920, + width: 1280 + }, + 2: { //topleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 0, + positionY: 0, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + }, + 3: { //middleleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 0, + positionY: 360, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + }, + 4: { //bottomleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 0, + positionY: 720, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + } + }, + "6boxbig": { + 1: { //big top right + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 720, + positionX: 640, + positionY: 0, + rotation: 0, + scaleX: 0.66666, + scaleY: 0.66666, + sourceHeight: 1080, + sourceWidth: 1920, + width: 1280 + }, + 2: { //topleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 0, + positionY: 0, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + }, + 3: { //middleleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 0, + positionY: 360, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + }, + 4: { //bottomleft + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 0, + positionY: 720, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + }, + 5: { //bottomcenter + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 640, + positionY: 720, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + }, + 6: { //bottomright + cropBottom: 0, + cropLeft: 0, + cropRight: 0, + cropTop: 0, + height: 360, + positionX: 1280, + positionY: 720, + rotation: 0, + scaleX: 0.3333333, + scaleY: 0.3333333, + sourceHeight: 1080, + sourceWidth: 1920, + width: 640 + } + }, +} + +module.exports = { + commandPrefix, + ptzPrefix, + userPermissions, + commandPermissions, + commandAlias, + commandScenes, + commandScenesCloud, + commandList, + timeRestrictedCommands, + customSceneCommands, + notifyScenes, + multiScenes, + multiCommands, + axisCameras, + onewayCommands, + onewayNotifications, + sceneAudioSource, + scenePositions, + globalMusicSource, + timeRestrictedScenes, + commandAliasConverted, + multiCustomCamScenes, + multiCustomCamScenesConverted, + throttledCommands, + throttleCommandLength, + twitchChannelList, + pauseGameChange, + pauseNotify, + pauseTwitchMarker, + announceChatSceneChange, + alveusTwitchID, + customCamCommandMapping, + customCommandAlias, + micGroups, + userBlacklist, + axisCameraCommandMapping, + pauseCloudSceneChange, + notifyHours, + restrictedHours +}; \ No newline at end of file diff --git a/src/connections/cameras.js b/src/connections/cameras.js new file mode 100644 index 0000000..382c4c2 --- /dev/null +++ b/src/connections/cameras.js @@ -0,0 +1,422 @@ +const DigestFetch = require("digest-fetch"); + +const Logger = require("../utils/logger"); +const config = require("../config/config"); + +/** + * Axis Camera Vapix Class + * + * https://www.axis.com/vapix-library/subjects/t10175981/section/t10036011/display + */ +class Axis { + #logger; + #host; + #client; + + /** + * Establishes a connection to an Axis camera + * + * @param {string} name Name of the camera (for logging) + * @param {string} host Hostname or IP address of the camera + * @param {string} username Username to authenticate with + * @param {string} password Password to authenticate with + */ + constructor(name, host, username, password) { + this.#logger = new Logger(`connections/cameras/${name}`); + this.#host = host; + this.#client = new DigestFetch(username, password); + } + + /** + * Make a GET request to the camera + * + * @param {string} endpoint Endpoint to make the request to + * @returns {Promise} Response body, or null if the request failed + * @private + */ + async #get(endpoint) { + try { + // Make the request + const response = await this.#client.fetch( + `http://${this.#host}${endpoint}`, + ); + const data = await response.text(); + + // If we got data, return the data + if (data !== null && data !== undefined) return data; + + // Otherwise, log what appears to be the error and return null + this.#logger.error(`Failed to GET ${endpoint}: ${data?.error?.code}`); + return null; + } catch (e) { + // If the request threw an error, log it and return null + this.#logger.error(`Failed to GET ${endpoint}: ${e}`); + return null; + } + } + + /** + * Make a POST request to the camera + * + * Parses the response as JSON + * + * @param {string} endpoint Endpoint to make the request to + * @param {any} body Request body (will be stringified as JSON) + * @returns {Promise} Response body, or null if the request failed + * @private + */ + async #post(endpoint, body) { + try { + // Make the request + const response = await this.client.fetch( + `http://${this.#host}${endpoint}`, + { + method: "post", + body: JSON.stringify(body), + headers: { "Content-Type": "application/json" }, + }, + ); + const data = await response.json(); + + // If we got data and no error, return the data + if (data !== null && data !== undefined && data.error === null) + return data; + + // Otherwise, log what appears to be the error and return null + this.#logger.error(`Failed to POST ${endpoint}: ${data?.error?.code}`); + return null; + } catch (e) { + // If the request threw an error, log it and return null + this.#logger.error(`Failed to POST ${endpoint}: ${e}`); + return null; + } + } + + /** + * Enable auto tracking for the camera + * + * @returns {Promise} + */ + async enableAutoTracking() { + const resp = await this.#post( + "/local/axis-ptz-autotracking/settings.fcgi", + { + apiVersion: "1.0", + context: "abc", + method: "setAutotrackingState", + params: { + enabled: true, + }, + }, + ); + return resp !== null; + } + + /** + * Disable auto tracking for the camera + * + * @returns {Promise} + */ + async disableAutoTracking() { + const resp = await this.#post( + "/local/axis-ptz-autotracking/settings.fcgi", + { + apiVersion: "1.0", + context: "abc", + method: "setAutotrackingState", + params: { + enabled: false, + }, + }, + ); + return resp !== null; + } + + /** + * Run a PTZ commands on the camera + * + * @param {Record} commands Commands to run + * @returns {Promise} + */ + async ptz(commands) { + const query = new URLSearchParams(commands); + const resp = await this.#get(`/axis-cgi/com/ptz.cgi?${query.toString()}`); + return resp !== null; + } + + /** + * Move the camera + * + * Expects values such as 25%, 50-90degrees, home, up, down, left, right, upleft, upright, downleft, downright, stop + * + * @param {string} direction Direction to move the camera + * @returns {Promise} + */ + async moveCamera(direction) { + return this.ptz({ move: direction }); + } + + /** + * Home the camera position + * + * @returns {Promise} + */ + async goHome() { + return this.moveCamera("home"); + } + + /** + * Stop the camera moving + * + * @returns {Promise} + */ + async stopCamera() { + return this.moveCamera("stop"); + } + + /** + * Pan the camera, relative to the current position + * + * @param {number} degrees Degrees to pan the camera (-360 to 360) + * @returns {Promise} + */ + async panCamera(degrees) { + return this.ptz({ rpan: degrees }); + } + + /** + * Pan the camera, to an exact position + * + * @param {number} degrees Degrees to pan the camera (-180 to 180) + * @returns {Promise} + */ + async panCameraExact(degrees) { + return this.ptz({ pan: degrees }); + } + + /** + * Tilt the camera, relative to the current position + * + * @param {number} degrees Degrees to tilt the camera (-360 to 360) + * @returns {Promise} + */ + async tiltCamera(degrees) { + return this.ptz({ rtilt: degrees }); + } + + /** + * Tilt the camera, to an exact position + * + * @param {number} degrees Degrees to tilt the camera (-180 to 180) + * @returns {Promise} + */ + async tiltCameraExact(degrees) { + return this.ptz({ tilt: degrees }); + } + + /** + * Zoom the camera, relative to the current zoom + * + * @param {number} steps Steps to zoom the camera (-9999 to 9999) + * @returns {Promise} + */ + async zoomCamera(steps) { + return this.ptz({ rzoom: steps }); + } + + /** + * Zoom the camera, to an exact zoom + * + * @param {number} steps Steps to zoom the camera (1 to 9999) + * @returns {Promise} + */ + async zoomCameraExact(steps) { + return this.ptz({ zoom: steps }); + } + + /** + * Focus the camera, relative to the current focus + * + * @param {number} steps Steps to focus the camera (-9999 to 9999) + * @returns {Promise} + */ + async focusCamera(steps) { + return this.ptz({ rfocus: steps }); + } + + /** + * Focus the camera, to an exact focus + * + * @param {number} steps Steps to focus the camera (1 to 9999) + * @returns {Promise} + */ + async focusCameraExact(steps) { + return this.ptz({ focus: steps }); + } + + /** + * Enable auto focus for the camera + * + * @returns {Promise} + */ + async enableAutoFocus() { + return this.ptz({ autofocus: "on" }); + } + + /** + * Disable auto focus for the camera + * + * @returns {Promise} + */ + async disableAutoFocus() { + return this.ptz({ autofocus: "off" }); + } + + /** + * Enable a continuous pan/tilt movement + * + * Set the pan/tilt speed to 0 to stop + * + * @param {number} pan Pan speed (-100 to 100) + * @param {number} tilt Tilt speed (-100 to 100) + * @returns {Promise} + */ + async continousPanTilt(pan, tilt) { + return this.ptz({ continuouspantiltmove: `${pan},${tilt}` }); + } + + /** + * Move the camera to a stored preset + * + * @param {string} name Name of the preset + * @returns {Promise} + */ + async goToPreset(name) { + return this.ptz({ gotoserverpresetname: name.toLowerCase() }); + } + + /** + * Set the movement speed of the camera + * + * @param {number} speed Speed of the camera (1 to 100) + * @returns {Promise} + */ + async setSpeed(speed) { + return this.ptz({ speed }); + } + + /** + * Run a speed dry cycle for the camera + * + * @returns {Promise} + */ + async speedDry() { + return this.ptz({ auxiliary: "speeddry" }); + } + + /** + * Set the IR cut filter state + * + * @param {"on" | "off" | "auto"} state State to set the IR cut filter to (on = day, off = night) + * @returns {Promise} + */ + async setIRCutFilter(state) { + return this.ptz({ ircutfilter: state }); + } + + /** + * Enable the IR light for the camera + * + * @returns {Promise} + */ + async enableIR() { + const resp = await this.#post("/axis-cgi/lightcontrol.cgi", { + apiVersion: "1.0", + context: "abc", + method: "enableLight", + params: { + lightID: "led0", + }, + }); + return resp !== null; + } + + /** + * Disable the IR light for the camera + * + * @returns {Promise} + */ + async disableIR() { + const resp = await this.#post("/axis-cgi/lightcontrol.cgi", { + apiVersion: "1.0", + context: "abc", + method: "disableLight", + params: { + lightID: "led0", + }, + }); + return resp !== null; + } + + /** + * Get the current position of the camera + * + * @returns {Promise<{ pan: number, tilt: number, zoom: number, focus: number, brightness: number, autofocus: "on" | "off", autoiris: "on" | "off" } | null>} + */ + async getPosition() { + // Get the raw position data + const resp = await this.#get("/axis-cgi/com/ptz.cgi?query=position"); + if (resp === null) return null; + + // Parse the data (=\r\n...) + const pairs = resp.replace(/\r/g, "").split("\n"); + const position = {}; + for (const pair of pairs) { + const [key, value] = pair.trim().split("="); + if (key) { + // Attempt to parse the value as a number, and store it + position[key] = isNaN(value) ? value : parseFloat(value); + } + } + return position; + } + + /** + * Get the current movement speed of the camera + * + * @returns {Promise} + */ + async getSpeed() { + // Get the raw speed data + const resp = await this.#get("/axis-cgi/com/ptz.cgi?query=speed"); + if (resp === null) return null; + + // Parse the data, ensuring we got back a number + return isNaN(resp) ? null : parseFloat(resp); + } +} + +/** + * Establishes connections to the Axis cameras + * + * `controller.connections.cameras` is an object of Axis camera connections + * + * @param {import("../controller")} controller + * @returns {Promise} + */ +module.exports = async (controller) => { + const cameras = config.axisCameras.reduce( + (obj, name) => ({ + ...obj, + [name]: new Axis( + name, + process.env[`AXIS_${name.toUpperCase()}_IP`], + process.env.AXIS_USERNAME, + process.env.AXIS_PASSWORD, + ), + }), + {}, + ); + + controller.connections.cameras = cameras; +}; diff --git a/src/connections/courier.js b/src/connections/courier.js new file mode 100644 index 0000000..9f62647 --- /dev/null +++ b/src/connections/courier.js @@ -0,0 +1,305 @@ +const { CourierClient } = require("@trycourier/courier"); + +const Logger = require("../utils/logger"); + +const logger = new Logger("connections/courier"); + +class Courier { + #client; + + /** + * Establishes a connection to the Courier API + * + * @param {string} token Authorization token for the Courier API + */ + constructor(token) { + this.#client = CourierClient({ authorizationToken: token }); + } + + /** + * Create a new Courier profile + * + * @param {string} name Name of the user + * @param {string} [email] Email address for the profile (optional) + * @param {string} [phoneNumber] Phone number for the profile (optional) + * @param {string} [discordUserID] Discord user ID for the profile (optional) + * @param {string} [discordChannelID] Discord channel ID for the profile (optional) + * @returns {Promise>["status"] | null>} + */ + async createProfile( + name, + email, + phoneNumber, + discordUserID, + discordChannelID, + ) { + // Base profile just has the name + const profile = { + recipientId: String(name), + profile: { + given_name: String(name), + }, + }; + + // Add the optional fields if they were provided + if (email) profile.profile.email = String(email); + if (phoneNumber) profile.profile.phone_number = String(phoneNumber); + if (discordUserID) + profile.profile.discord = { + ...profile.profile.discord, + user_id: String(discordUserID), + }; + if (discordChannelID) + profile.profile.discord = { + ...profile.profile.discord, + channel_id: String(discordChannelID), + }; + + try { + const { status } = await this.#client.mergeProfile(profile); + logger.log(`Created profile ${name}: ${status}`); + return status; + } catch (e) { + logger.error(`Error creating profile ${name}`, e); + return null; + } + } + + /** + * Create a new list in Courier + * + * @param {string} name Name of the list + * @param {string} [displayName] Display name of the list (optional) + * @returns {Promise} + */ + async createList(name, displayName) { + // Fallback to the name if no display name was provided + displayName = displayName || name; + + try { + await this.#client.lists.put(name, { + name: displayName, + }); + logger.log(`Created list ${name} (${displayName})`); + return true; + } catch (e) { + logger.error(`Error creating list ${name} (${displayName})`, e); + return false; + } + } + + /** + * Subscribe a user to a list in Courier + * + * @param {string} list Name of the list + * @param {string} user Name of the user + * @returns {Promise} + */ + async subscribeToList(list, user) { + try { + await this.#client.lists.subscribe(list, user); + logger.log(`Subscribed ${user} to ${list}`); + return true; + } catch (e) { + logger.error(`Error subscribing ${user} to ${list}`, e); + return false; + } + } + + /** + * Unsubscribe a user from a list in Courier + * + * @param {string} list Name of the list + * @param {string} user Name of the user + * @returns {Promise} + */ + async unsubscribeToList(list, user) { + try { + await this.#client.lists.unsubscribe(listlistName, user); + logger.log(`Unsubscribed ${user} from ${list}`); + return true; + } catch (e) { + logger.error(`Error unsubscribing ${user} from ${list}`, e); + return false; + } + } + + /** + * Get the users subscribed to a list in Courier + * + * @param {string} list Name of the list + * @returns {Promise> | null>} + */ + async getUsers(list) { + try { + const response = await this.#client.lists.getSubscriptions(list); + logger.log(`Users of ${list}: ${JSON.stringify(response)}`); + return response; + } catch (e) { + logger.error(`Error getting users of ${list}`, e); + return null; + } + } + + /** + * Get a user's profile from Courier + * + * @param {string} name Name of the user + * @returns {Promise>["profile"] | null>} + */ + async getUser(name) { + try { + const { profile } = await this.#client.getProfile({ + recipientId: name, + }); + logger.log(`User profile of ${name}: ${JSON.stringify(profile)}`); + return profile; + } catch (e) { + logger.error(`Error getting user profile of ${name}`, e); + return null; + } + } + + /** + * Send a notification to a list in Courier + * + * @param {string} event Name of the event being sent + * @param {string} list Name of the list to send to + * @param {string} message Message to include in the notification + * @param {object} [override] Override data for the notification (optional) + * @returns {Promise>["messageId"] | null>} + */ + async sendListNotification(event, list, message, override) { + try { + // Create the payload, with the optional override + const payload = { + event: event, + list: list, + data: { + message, + }, + // override: { + // discord: { + // body: { + // embed: { + // title: message.title + // description: message.message || "", + // fields: [ + // { + // name: "timestamp", + // value: message.timestamp || "", + // inline: true, + // } + // ], + // }, + // }, + // }, + // }, + }; + if (override) payload.override = override; + + // Send the notification + const { messageId } = await this.#client.lists.send(payload); + logger.log(`Sent notification to list ${list}`, messageId); + return messageId; + } catch (e) { + logger.error(`Error sending notification to list ${list}`, e); + return null; + } + } + + /** + * Send a Discord notification to a list in Courier + * + * @param {string} event Name of the event being sent + * @param {string} list Name of the list to send to + * @param {string} message Message to include in the notification + * @param {string} [description] Description to include in the notification (optional) + * @param {{ name: string, value: string }[]} [fields] Fields to include in the notification (optional) + * @returns {Promise>["messageId"] | null>} + */ + async sendListNotificationDiscord(event, list, message, description, fields) { + try { + // Create the payload, with the optional description and fields + const payload = { + event: event, + list: list, + data: { + message, + description, + }, + override: { + discord: { + body: { + embed: { + title: message.message || "", + description: description || message.message || "", + }, + }, + }, + }, + }; + if (fields) payload.override.discord.body.embed.fields = fields; + + // Send the notification + const { messageId } = await this.#client.lists.send(payload); + logger.log(`Sent Discord notification to list ${list}`, messageId); + return messageId; + } catch (e) { + logger.error(`Error sending Discord notification to list ${list}`, e); + return null; + } + } + + /** + * Send a notification to a user in Courier + * + * @param {string} brand ID of the brand for the notification + * @param {string} event ID of the event for the notification + * @param {string} recipient ID of the recipient for the notification + * @param {{ email: string, discord?: { user_id: string } }} profile User information for the notification + * @param {string} message Message to include in the notification + * @param {object} [override] Override data for the notification (optional) + * @returns {Promise>["messageId"] | null>} + */ + async sendNotification(brand, event, recipient, profile, message, override) { + try { + let payload = { + brand: brand, + eventId: event, + recipientId: recipient, + profile: profile, + data: message, + }; + if (override) payload.override = override; + + // Send the notification + const { messageId } = await this.#client.send(payload); + logger.log(`Sent notification to ${recipient}`, messageId); + return messageId; + } catch (e) { + logger.error(`Error sending notification to ${recipient}`, e); + return null; + } + } +} + +/** + * Establishes connections to the Courier API + * + * `controller.connections.courier` is the Courier instance + * + * @param {import("../controller")} controller + * @returns {Promise} + */ +module.exports = async (controller) => { + if (!process.env.COURIER_KEY) { + logger.warn( + "No Courier API key found. Notifications will not be sent. To enable notifications, set the COURIER_KEY environment variable.", + ); + return; + } + + const courier = new Courier(process.env.COURIER_KEY); + controller.connections.courier = courier; +}; diff --git a/src/connections/database.js b/src/connections/database.js new file mode 100644 index 0000000..2aea96c --- /dev/null +++ b/src/connections/database.js @@ -0,0 +1,57 @@ +const { readFileSync, writeFileSync, existsSync } = require("node:fs"); +const { join } = require("node:path"); + +const onChangePromise = import("on-change"); // ESM-only module + +const defaults = { cloudServer: "space", timeRestrictionDisabled: false }; +const file = "data/database.json"; + +class Database { + #data = {}; + #file = ""; + + /** + * Create a new database connection + * + * Defaults to an empty object if the file does not exist + * Otherwise, loads the JSON file and parses it + * + * @param {string} file File to load from and save the database to + * @returns {Promise} + */ + constructor(file) { + this.#file = file; + if (existsSync(file)) this.#data = JSON.parse(readFileSync(file, "utf8")); + + // Save database on exit + process.on("exit", () => { + this.#save(); + }); + + // Load on-change module as it is ESM-only + // Observe the data and save on any change + return onChangePromise.then(({ default: onChange }) => + onChange(this.#data, () => this.#save()), + ); + } + + #save() { + writeFileSync(this.#file, JSON.stringify(this.#data)); + } +} + +/** + * Loads the database and adds it to the controller + * + * `controller.connections.database` is the database connection + * + * @param {import("../controller")} controller + * @returns {Promise} + */ +module.exports = async (controller) => { + const database = await new Database(join(process.cwd(), file)); + for (const [key, value] of Object.entries(defaults)) database[key] ??= value; + + // Add database to controller + controller.connections.database = database; +}; diff --git a/src/connections/obs.js b/src/connections/obs.js new file mode 100644 index 0000000..1c92f0e --- /dev/null +++ b/src/connections/obs.js @@ -0,0 +1,1286 @@ +const { default: OBSWebSocketNew } = require("obs-websocket-js"); +const OBSWebSocketOld = require("obs-websocket-js-27"); // npm i obs-websocket-js-27@npm:obs-websocket-js@4.0.3 + +const utilsModule = require("../utils/utilsModule"); + +const connections = { + localAlveus: { + name: "AlveusServer", + address: process.env.OBS_WS, + password: process.env.OBS_KEY, + }, + localNuthouse: { + name: "NuthouseServer", + address: process.env.OBS_WS_NUTHOUSE, + password: process.env.OBS_KEY_NUTHOUSE, + }, + localTest: { + name: "TestServer", + address: process.env.OBS_WS_TEST, + password: process.env.OBS_KEY_TEST, + }, + cloudAlveus: { + name: "CloudAlveusServer", + address: process.env.OBS_WS_ALVEUS_CLOUD, + password: process.env.OBS_KEY_ALVEUS_CLOUD, + old: true, + }, + cloudMaya: { + name: "CloudMayaServer", + address: process.env.OBS_WS_MAYA_CLOUD, + password: process.env.OBS_KEY_MAYA_CLOUD, + old: true, + }, + cloudSpace: { + name: "CloudSpaceServer", + address: process.env.OBS_WS_SPACE_CLOUD, + password: process.env.OBS_KEY_SPACE_CLOUD, + }, +}; + +/** + * OBS Websocket Class + * + * https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md + * https://github.com/obsproject/obs-websocket/blob/4.x-compat/docs/generated/protocol.md#table-of-contents + */ +class OBS { + client = null; + name = null; + ipAddress = null; + password = null; + studioMode = null; + transitioning = false; + currentScene = null; + isStreaming = null; + sceneList = []; + retryAmount = 99999999; + retryCounter = 0; + retryTimeout = 250; + maxTimeout = 30000; + utils = null; + connected = false; + enableReconnect = true; + + constructor(name, ipAddress, password, oldWS) { + this.name = name; + let utilsName = ""; + if (name == null || name == "") { + utilsName = `OBSModule`; + } else { + utilsName = `OBS Module(${name})` || `OBSModule`; + } + this.ipAddress = ipAddress; + this.password = password; + this.oldWS = oldWS; + // this.utils.setPrefix(`[${this.name}]`); + this.utils = new utilsModule(`[${utilsName}]`); + } + + async createClient() { + const self = this; + try { + self.client = this.oldWS ? new OBSWebSocketOld() : new OBSWebSocketNew(); + + if (self.oldWS) { + self.client.on("StudioModeSwitched", (event) => { + self.utils.log(`StudioModeSwitched: ${JSON.stringify(event)}`); + self.studioMode = event["new-state"]; + }); + self.client.on("TransitionBegin", (event) => { + //self.utils.log(`SceneTransitionStarted: ${JSON.stringify(event)}`); + self.transitioning = true; + }); + self.client.on("SwitchScenes", (event) => { + // self.utils.log(`SceneNameChanged: ${JSON.stringify(event)}`); + // let oldName = event.oldSceneName; + // let newName = event.sceneName; + self.getCurrentSceneList(); + }); + self.client.on("SceneCollectionChanged", (event) => { + //self.utils.log(`CurrentSceneCollectionChanged: ${JSON.stringify(event)}`); + // let newCollectionName = event.sceneCollectionName; + self.getCurrentSceneList(); + }); + self.client.on("StreamStarted", (event) => { + //self.utils.log(`StreamStateChanged: ${JSON.stringify(event)}`); + // let newCollectionName = event.sceneCollectionName; + self.isStreaming = true; + }); + self.client.on("StreamStopped", (event) => { + //self.utils.log(`StreamStateChanged: ${JSON.stringify(event)}`); + // let newCollectionName = event.sceneCollectionName; + self.isStreaming = false; + }); + self.client.on("AuthenticationSuccess", (event) => { + self.utils.log(`AuthenticationSuccess`); + self.connected = true; + self.retryTimeout = 250; + self.retryCounter = 0; + self.getCurrentSceneList(); + self.fetchStudioMode(); + self.fetchStreamStatus(); + }); + self.client.on("AuthenticationFailure", (error) => { + self.connected = false; + self.utils.log(`AuthenticationFailure: ${error}`); + self.client.disconnect(); + self.reconnect(); + }); + self.client.on("ConnectionOpened", () => { + //self.utils.log(`ConnectionOpened`); + }); + self.client.on("ConnectionClosed", (error) => { + self.connected = false; + self.utils.log(`ConnectionClosed: ${error}`); + self.reconnect(); + }); + self.client.on("ConnectionError", (error) => { + self.utils.log(`ConnectionError: ${error}`); + self.client.disconnect(); + }); + } else { + self.client.on("CurrentProgramSceneChanged", (event) => { + //used in client.scenechange(); + //self.utils.log(`CurrentProgramSceneChanged: ${JSON.stringify(event)}`); + }); + self.client.on("CurrentPreviewSceneChanged", (event) => { + //self.utils.log(`CurrentPreviewSceneChanged: ${JSON.stringify(event)}`); + }); + self.client.on("StudioModeStateChanged", (event) => { + //self.utils.log(`StudioModeStateChanged: ${JSON.stringify(event)}`); + self.studioMode = event.studioModeEnabled; + }); + self.client.on("SceneTransitionStarted", (event) => { + //self.utils.log(`SceneTransitionStarted: ${JSON.stringify(event)}`); + self.transitioning = true; + }); + self.client.on("SceneTransitionVideoEnded", (event) => { + //self.utils.log(`SceneTransitionVideoEnded: ${JSON.stringify(event)}`); + //self.transitioning = false; + }); + self.client.on("SceneNameChanged", (event) => { + //self.utils.log(`SceneNameChanged: ${JSON.stringify(event)}`); + // let oldName = event.oldSceneName; + // let newName = event.sceneName; + self.getCurrentSceneList(); + }); + self.client.on("CurrentSceneCollectionChanged", (event) => { + //self.utils.log(`CurrentSceneCollectionChanged: ${JSON.stringify(event)}`); + // let newCollectionName = event.sceneCollectionName; + self.getCurrentSceneList(); + }); + self.client.on("InputCreated", (event) => { + //self.utils.log(`InputCreated: ${JSON.stringify(event)}`); + // let name = event.inputName; + // let type = event.inputKind; + // let settings = event.inputSettings; + // self.getCurrentSceneList(); + }); + self.client.on("SceneItemCreated", (event) => { + //self.utils.log(`SceneItemCreated: ${JSON.stringify(event)}`); + // let sceneName = event.sceneName; + // let sourceName = event.sourceName; + // let sceneItemId = event.sceneItemId; + // let sceneItemIndex = event.sceneItemIndex; + self.getCurrentSceneList(); + }); + self.client.on("SceneItemRemoved", (event) => { + //self.utils.log(`SceneItemRemoved: ${JSON.stringify(event)}`); + // let sceneName = event.sceneName; + // let sourceName = event.sourceName; + // let sceneItemId = event.sceneItemId; + self.getCurrentSceneList(); + }); + self.client.on("SceneItemListReindexed", (event) => { + //self.utils.log(`SceneItemListReindexed: ${JSON.stringify(event)}`); + // let sceneName = event.sceneName; + // let sceneItems = event.sceneItems; //array of scene item objects + self.getCurrentSceneList(); + }); + self.client.on("SceneItemEnableStateChanged", (event) => { + //self.utils.log(`SceneItemEnableStateChanged: ${JSON.stringify(event)}`); + // let sceneName = event.sceneName; + // let sceneItemId = event.sceneItemId; + // let sceneItemEnabled = event.sceneItemEnabled; + self.getCurrentSceneList(); + }); + self.client.on("SceneItemLockStateChanged", (event) => { + //self.utils.log(`SceneItemLockStateChanged: ${JSON.stringify(event)}`); + // let sceneName = event.sceneName; + // let sceneItemId = event.sceneItemId; + // let sceneItemLocked = event.sceneItemLocked; + self.getCurrentSceneList(); + }); + self.client.on("SceneItemTransformChanged", (event) => { + //self.utils.log(`SceneItemTransformChanged: ${JSON.stringify(event)}`); + // let sceneName = event.sceneName; + // let sceneItemId = event.sceneItemId; + // let sceneItemTransform = event.sceneItemTransform; + self.getCurrentSceneList(); + }); + self.client.on("StreamStateChanged", (event) => { + //self.utils.log(`StreamStateChanged: ${JSON.stringify(event)}`); + // let newCollectionName = event.sceneCollectionName; + self.isStreaming = event.outputActive; + }); + + self.client.on("ConnectionOpened", () => { + self.utils.log(`ConnectionOpened`); + }); + self.client.on("ConnectionClosed", (error) => { + self.connected = false; + self.utils.log(`ConnectionClosed: ${error}`); + self.reconnect(); + }); + self.client.on("ConnectionError", (error) => { + self.utils.log(`ConnectionError: ${error}`); + self.client.disconnect(); + }); + self.client.on("Identified", (data) => { + //data.negotiatedRpcVersion + self.utils.log(`Connected and Identified: ${JSON.stringify(data)}`); + self.connected = true; + self.retryTimeout = 250; + self.retryCounter = 0; + self.getCurrentSceneList(); + self.fetchStudioMode(); + self.fetchStreamStatus(); + }); + } + self.connect(); + } catch (e) { + self.utils.log(`Error creating client: ${JSON.stringify(e)}`); + //throw `Error creating client: ${e}`; + } + return this.client; + } + async isConnected() { + return this.connected; + } + async disconnect() { + this.setAutoReconnect(false); + this.client.disconnect(); + } + async connect() { + try { + let connectInfo = null; + if (this.oldWS) { + if (this.password == null || this.password == "") { + connectInfo = await this.client.connect({ + address: this.ipAddress, + password: "", + }); + } else { + connectInfo = await this.client.connect({ + address: this.ipAddress, + password: this.password, + }); + } + } else { + if (this.password == null || this.password == "") { + connectInfo = await this.client.connect(this.ipAddress); + } else { + connectInfo = await this.client.connect( + this.ipAddress, + this.password, + ); + } + } + return connectInfo; + } catch (e) { + //this.utils.log(`Error Connecting: ${JSON.stringify(e)}`); + return null; + } + } + reconnect() { + if (!this.enableReconnect) { + return; + } + if (this.retryCounter <= this.retryAmount) { + this.utils.log( + `Retrying to connect attempt: ${this.retryCounter} - ${this.retryTimeout}ms`, + ); + let length = this.retryTimeout + this.retryTimeout; + if (length > this.maxTimeout) { + this.retryTimeout = this.maxTimeout; + } else { + this.retryTimeout = length; + } + setTimeout(this.connect.bind(this), length); + this.retryCounter++; + } + } + setAutoReconnect(boolean) { + this.utils.log(`Set Auto Reconnect: ${boolean}`); + if (boolean == true) { + this.enableReconnect = true; + //reconnect if not connected + if (!this.connected) { + this.reconnect(); + } + } else { + this.enableReconnect = false; + } + } + async startStream() { + try { + let response = null; + if (this.oldWS) { + response = await this.client.send("StartStreaming"); + } else { + response = await this.client.call("StartStream"); + } + this.utils.log(`Start Stream): ${JSON.stringify(response)}`); + return true; + } catch (e) { + this.utils.log(`Error Starting Stream: ${JSON.stringify(e)}`); + return null; + } + } + async stopStream() { + try { + let response = null; + if (this.oldWS) { + response = await this.client.send("StopStreaming"); + } else { + response = await this.client.call("StopStream"); + } + this.utils.log(`Stop Stream): ${JSON.stringify(response)}`); + return true; + } catch (e) { + this.utils.log(`Error Stopping Stream: ${JSON.stringify(e)}`); + return null; + } + } + sceneChange(func) { + let self = this; + self.client.on("CurrentProgramSceneChanged", (event) => { + let oldSceneName = self.currentScene; + //event.sceneName + // If in studio mode, do not notify of scene change unless transition button used + // console.log("scenechange",event,self.studioMode,self.transitioning); + if (self.studioMode) { + if (self.transitioning) { + //manual transition, actual scene change + self.currentScene = event.sceneName; + self.getCurrentSceneList(); + func(event.sceneName, oldSceneName); + // console.log("Real Scene Change",event.sceneName); + } + //do nothing + } else { + // console.log("Real Scene Change",event.sceneName); + self.currentScene = event.sceneName; + self.getCurrentSceneList(); + func(event.sceneName, oldSceneName); + } + self.transitioning = false; + }); + } + async fetchStudioMode() { + try { + let response = null; + if (this.oldWS) { + response = await this.client.send("GetStudioModeStatus"); + // this.utils.log(`Current Studio Mode: ${JSON.stringify(response)}`); + if (response && response["studio-mode"] !== null) { + this.studioMode = response["studio-mode"]; + return response["studio-mode"]; + } else if (response == null) { + return null; + } + } else { + response = await this.client.call("GetStudioModeEnabled"); + // this.utils.log(`Current Studio Mode: ${JSON.stringify(response)}`); + if (response && response.studioModeEnabled !== null) { + this.studioMode = response.studioModeEnabled; + return response.studioModeEnabled; + } else if (response == null) { + return null; + } + } + } catch (e) { + this.utils.log(`Error Getting Studio Mode: ${JSON.stringify(e)}`); + return null; + } + } + async isStudioMode() { + if (this.studioMode == null) { + return await this.fetchStudioMode(); + } else { + return this.studioMode; + } + } + async fetchCurrentScene() { + try { + let response = null; + if (this.oldWS) { + response = await this.client.send("GetCurrentScene"); + // this.utils.log(`Current Scene: ${JSON.stringify(response.name)}`); + if (response && response.name !== null) { + this.currentScene = response.name; + return response.name; + } else if (response == null) { + return null; + } + } else { + response = await this.client.call("GetCurrentProgramScene"); + // this.utils.log(`Current Scene: ${JSON.stringify(response)}`); + if (response && response.currentProgramSceneName !== null) { + this.currentScene = response.currentProgramSceneName; + return response.currentProgramSceneName; + } else if (response == null) { + return null; + } + } + } catch (e) { + this.utils.log(`Error Getting Current Scene: ${JSON.stringify(e)}`); + return null; + } + } + async getScene() { + if (this.currentScene == null) { + return await this.fetchCurrentScene(); + } else { + return this.currentScene; + } + } + async isLive() { + if (this.currentScene == null) { + return await this.fetchStreamStatus(); + } else { + return this.isStreaming; + } + } + async fetchStreamStatus() { + //https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#getstreamstatus + try { + let response = null; + if (this.oldWS) { + response = await this.client.send("GetStreamingStatus"); + this.utils.log(`Stream Status: ${JSON.stringify(response)}`); + if (response && response.streaming !== null) { + this.isStreaming = response.streaming; + return response.streaming; + } else if (response == null) { + return null; + } + } else { + response = await this.client.call("GetStreamStatus"); + this.utils.log(`Stream Status: ${JSON.stringify(response)}`); + if (response && response.outputActive !== null) { + this.isStreaming = response.outputActive; + return response.outputActive; + } else if (response == null) { + return null; + } + } + } catch (e) { + this.utils.log(`Error Getting Stream Status: ${JSON.stringify(e)}`); + return null; + } + } + async restartSource(sourceName) { + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NEXT + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PREVIOUS + let param = { + inputName: sourceName, + mediaAction: "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART", + }; + try { + let response = null; + if (this.oldWS) { + response = await this.client.send("RestartMedia", { + sourceName: sourceName, + }); + } else { + response = await this.client.call("TriggerMediaInputAction", param); + } + this.utils.log( + `Restart Source (${sourceName}): ${JSON.stringify(response)}`, + ); + return true; + } catch (e) { + this.utils.log( + `Error Restarting Source (${sourceName}): ${JSON.stringify(e)}`, + ); + return null; + } + } + async nextMediaSource(sourceName) { + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NEXT + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PREVIOUS + let param = { + inputName: sourceName, + mediaAction: "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NEXT", + }; + try { + let response = null; + if (this.oldWS) { + // response = await this.client.send("nextMediaSource", { + // sourceName: sourceName, + // }); + } else { + response = await this.client.call("TriggerMediaInputAction", param); + } + this.utils.log( + `nextMediaSource(${sourceName}): ${JSON.stringify(response)}`, + ); + return true; + } catch (e) { + this.utils.log( + `Error nextMediaSource (${sourceName}): ${JSON.stringify(e)}`, + ); + return null; + } + } + async prevMediaSource(sourceName) { + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PLAY + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PAUSE + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_STOP + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_RESTART + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_NEXT + //OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PREVIOUS + let param = { + inputName: sourceName, + mediaAction: "OBS_WEBSOCKET_MEDIA_INPUT_ACTION_PREVIOUS", + }; + try { + let response = null; + if (this.oldWS) { + // response = await this.client.send("prevMediaSource", { + // sourceName: sourceName,setScene( + // }); + } else { + response = await this.client.call("TriggerMediaInputAction", param); + } + this.utils.log( + `prevMediaSource(${sourceName}): ${JSON.stringify(response)}`, + ); + return true; + } catch (e) { + this.utils.log( + `Error prevMediaSource (${sourceName}): ${JSON.stringify(e)}`, + ); + return null; + } + } + async setScene(sceneName) { + let self = this; + let param = { sceneName: sceneName }; + try { + let response = null; + if (self.oldWS) { + response = await self.client.send("SetCurrentScene", { + "scene-name": sceneName, + }); + } else { + response = await self.client.call("SetCurrentProgramScene", param); + } + //no result + self.utils.log(`Set Scene: ${sceneName}`); + return true; + } catch (e) { + self.utils.log( + `Error Setting Scene (${sceneName}): ${JSON.stringify(e)}`, + ); + return null; + } + } + async restartSceneItem(sceneName, sourceName) { + let self = this; + try { + if (self.oldWS) { + //old version + let params = { + "scene-name": sceneName, + item: sourceName, + visible: false, + }; + let response = await self.client.send("SetSceneItemProperties", params); + setTimeout(async () => { + params.visible = true; + await self.client.send("SetSceneItemProperties", params); + }, 2000); + return true; + } else { + let param = { + sceneName: sceneName, + sourceName: sourceName, + }; + let item = await self.findSceneItem(sceneName, sourceName); + //sceneItemEnabled: true, sceneItemId: 3, sceneItemIndex: 0, sourceName: 'Georgie Name', + //sourceType: 'OBS_SOURCE_TYPE_INPUT', groupName: 'test2', searchTerm: 'georgie', sceneName: 'test + self.utils.log( + `restartSceneItem (${sceneName},${sourceName}): ${item.sceneItemId} ${item.sceneItemEnabled}`, + ); + if (!item.sceneItemEnabled) { + //dont reset if its already off + return false; + } + if (item.groupName) { + //use group name if found under a group + param.sceneName = item.groupName; + } else { + param.sceneName = item.sceneName; + } + param.sourceName = item.sourceName; + param.sceneItemId = item.sceneItemId; + param.sceneItemEnabled = false; + await self.client.call("SetSceneItemEnabled", param); + setTimeout(async () => { + param.sceneItemEnabled = true; + await self.client.call("SetSceneItemEnabled", param); + }, 2000); + return true; + } + } catch (e) { + self.utils.log( + `Error restartSceneItem (${sceneName},${sourceName}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async setSourceNetworkCache(sourceName, networkCacheAmount) { + //for VLC sources + let self = this; + try { + let response = null; + if (self.oldWS) { + //old version + response = await self.client.send("SetSourceSettings", { + sourceName: sourceName, + //sourceType:"vlc_source", //media: ffmpeg_source + sourceSettings: { network_caching: networkCacheAmount }, + }); + } else { + //new version + response = await self.client.call("SetInputSettings", { + inputName: sourceName, + overlay: true, + inputSettings: { network_caching: networkCacheAmount }, + }); + } + return response; + } catch (e) { + self.utils.log( + `Error setSourceNetworkCache (${sourceName},${networkCacheAmount}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async toggleMute(sourceName) { + let self = this; + let param = { inputName: sourceName }; + try { + let response = null; + if (self.oldWS) { + response = await self.client.send("ToggleMute", { + source: sourceName, + }); + } else { + response = await self.client.call("ToggleInputMute", param); + } + //no result + self.utils.log(`Toggle Mute: ${sourceName}`); + return true; + } catch (e) { + self.utils.log(`Error Toggle Mute (${sourceName}): ${JSON.stringify(e)}`); + return null; + } + } + async getMute(sourceName) { + let self = this; + let param = { inputName: sourceName }; + try { + let response = null; + if (self.oldWS) { + response = await self.client.send("getMute", {"source": sourceName}); + } else { + response = await self.client.call("GetInputMute", param); + } + let booleanStatus = response?.inputMuted ?? null; + self.utils.log(`Get Mute: ${sourceName}-`,response); + return booleanStatus; + } catch (e) { + self.utils.log(`Error Get Mute (${sourceName}}): ${JSON.stringify(e)}`); + return null; + } + } + async setMute(sourceName, booleanStatus) { + let self = this; + let param = { inputName: sourceName, inputMuted: booleanStatus }; + try { + let response = null; + if (self.oldWS) { + response = await self.client.send("SetMute", { + source: sourceName, + mute: booleanStatus, + }); + } else { + response = await self.client.call("SetInputMute", param); + } + //no result + self.utils.log(`Set Mute: ${sourceName}-${booleanStatus}`); + return true; + } catch (e) { + self.utils.log( + `Error Set Mute (${sourceName}-${booleanStatus}): ${JSON.stringify(e)}`, + ); + return null; + } + } + async setInputVolume(sourceName, inputVolumeDb) { + let self = this; + //inputVolumeDb >= -100, <= 26 + //inputVolumeMul >= 0, <= 20 + let param = { inputName: sourceName, inputVolumeDb: inputVolumeDb }; + try { + let response = null; + if (self.oldWS) { + // response = await self.client.send("SetMute", { + // "source": sourceName, + // "mute":booleanStatus + // }); + } else { + response = await self.client.call("SetInputVolume", param); + } + //no result + self.utils.log(`SetInputVolume: ${sourceName}-${inputVolumeDb}`); + return true; + } catch (e) { + self.utils.log( + `Error SetInputVolume (${sourceName}-${inputVolumeDb}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async getInputVolume(sourceName) { + let self = this; + //inputVolumeDb >= -100, <= 26 + //inputVolumeMul >= 0, <= 20 + let param = { inputName: sourceName }; + try { + let response = null; + if (self.oldWS) { + // response = await self.client.send("SetMute", { + // "source": sourceName, + // "mute":booleanStatus + // }); + } else { + response = await self.client.call("GetInputVolume", param); + } + let dbVolume = response.inputVolumeDb; + self.utils.log(`GetInputVolume: ${sourceName}-`,response); + return dbVolume; + } catch (e) { + self.utils.log(`Error GetInputVolume (${sourceName}): ${JSON.stringify(e)}`); + return null; + } + } + async getInputSettings(sourceName) { + let self = this; + let param = { inputName: sourceName }; + try { + let response = null; + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + response = await self.client.call("GetInputSettings", param); + } + //no result + self.utils.log(`getInputSettings: ${sourceName}`, response); + return response; + } catch (e) { + self.utils.log( + `Error getInputSettings (${sourceName}): ${JSON.stringify(e)}`, + ); + return null; + } + } + async findSceneItem(sceneName, sourceName) { + let self = this; + try { + let sceneList = await self.getSceneItemList(sceneName); + let item = null; + //find exact name match + for (let scene of sceneList) { + if (scene.sourceName == sourceName) { + item = scene; + item.sceneName = sceneName; + break; + } + } + if (item == null) { + //try and find closest match + let cleanName = sourceName.toLowerCase().trim(); + for (let scene of sceneList) { + let name = scene.sourceName; + if (name.toLowerCase().trim().includes(cleanName)) { + item = scene; + item.searchTerm = sourceName; + item.sceneName = sceneName; + break; + } + } + } + //no result + self.utils.log( + `findSceneItem: ${sceneName}-${sourceName}`, + item.sceneItemId, + ); + return item; + } catch (e) { + self.utils.log( + `Error findSceneItem (${sceneName}-${sourceName}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async getSceneItemId(sceneName, sourceName) { + let self = this; + try { + let sceneList = await self.getSceneItemList(sceneName); + let id = null; + //find exact name match + for (let scene of sceneList) { + if (scene.sourceName == sourceName) { + id = scene.sceneItemId; + break; + } + } + if (id == null) { + //try and find closest match + let cleanName = sourceName.toLowerCase().trim(); + for (let scene of sceneList) { + let name = scene.sourceName; + if (name.toLowerCase().trim().includes(cleanName)) { + id = scene.sceneItemId; + break; + } + } + } + //no result + self.utils.log(`getSceneItemId: ${sceneName}-${sourceName}`, id); + return id; + } catch (e) { + self.utils.log( + `Error getSceneItemId (${sceneName}-${sourceName}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async getSceneItemIdExact(sceneName, sourceName) { + let self = this; + let param = { + sceneName: sceneName, + sourceName: sourceName, + }; + try { + let response = null; + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + response = await self.client.call("GetSceneItemId", param); + } + //no result + self.utils.log(`getSceneItemId: ${sceneName}-${sourceName}`, response); + return response.sceneItemId; + } catch (e) { + self.utils.log( + `Error getSceneItemId (${sceneName}-${sourceName}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async getSceneItemTransform(sceneName, sourceName) { + let self = this; + try { + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + let sceneItemId = await self.getSceneItemId(sceneName, sourceName); + let param = { + sceneName: sceneName, + sceneItemId: sceneItemId, + }; + let responseTransform = await self.client.call( + "GetSceneItemTransform", + param, + ); + self.utils.log( + `getSceneItemTransform (${sceneName},${sourceName},${sceneItemId}): ${responseTransform}`, + ); + return responseTransform; + } + } catch (e) { + self.utils.log( + `Error getSceneItemTransform (${sceneName},${sourceName},${sceneItemId}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async setSceneItemTransform(sceneName, sourceName, transform) { + let self = this; + try { + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + let sceneItemId = await self.getSceneItemId(sceneName, sourceName); + let param = { + sceneName: sceneName, + sceneItemId: sceneItemId, + sceneItemTransform: transform, + }; + let responseTransform = await self.client.call( + "SetSceneItemTransform", + param, + ); + self.utils.log( + `SetSceneItemTransform (${sceneName},${sourceName},${sceneItemId}): ${JSON.stringify( + transform, + )}`, + ); + return responseTransform; + } + } catch (e) { + self.utils.log( + `Error SetSceneItemTransform (${sceneName},${sourceName},${sceneItemId}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async setSceneItemIdTransform(sceneName, sceneItemId, transform) { + let self = this; + try { + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + let param = { + sceneName: sceneName, + sceneItemId: sceneItemId, + sceneItemTransform: transform, + }; + let responseTransform = await self.client.call( + "SetSceneItemTransform", + param, + ); + // self.utils.log(`setSceneItemIdTransform (${sceneName},${sceneItemId}): ${JSON.stringify(transform)}`); + return responseTransform; + } + } catch (e) { + self.utils.log( + `Error setSceneItemIdTransform (${sceneName},${sceneItemId}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async getSceneItemList(sceneName) { + let self = this; + try { + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + let param = { sceneName: sceneName }; + let sceneItems = await self.client.call("GetSceneItemList", param); + sceneItems = sceneItems.sceneItems || sceneItems; + // self.utils.log(`GetSceneItemList (${sceneName}): ${sceneItems}`); + // { + // inputKind: null, + // isGroup: false, + // sceneItemBlendMode: 'OBS_BLEND_NORMAL', + // sceneItemEnabled: false, + // sceneItemId: 4, + // sceneItemIndex: 3, + // sceneItemLocked: false, + // sceneItemTransform: [Object], + // sourceName: 'Georgie', + // sourceType: 'OBS_SOURCE_TYPE_SCENE' + // } + let allItems = []; + for (let item of sceneItems) { + allItems.push(item); + if (item.isGroup) { + let name = item.sourceName; + let groupItems = await self.getGroupItemList(name); + allItems = allItems.concat(groupItems); + } + } + return allItems; + } + } catch (e) { + self.utils.log( + `Error GetSceneItemList (${sceneName}): ${JSON.stringify(e)}`, + ); + return null; + } + } + async getCurrentSceneList() { + let self = this; + try { + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + let currentScene = await self.fetchCurrentScene(); + if (currentScene == null) { + return null; + } + let sceneList = await self.getSceneItemList(currentScene); + self.sceneList = sceneList; + return sceneList; + } + } catch (e) { + self.utils.log( + `Error GetSceneItemList (${sceneName}): ${JSON.stringify(e)}`, + ); + return null; + } + } + async getGroupItemList(groupName) { + let self = this; + try { + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + let param = { sceneName: groupName }; + let groupItems = await self.client.call("GetGroupSceneItemList", param); + groupItems = groupItems.sceneItems || groupItems; + for (let item of groupItems) { + item.groupName = groupName; + } + return groupItems; + } + } catch (e) { + self.utils.log( + `Error GetGroupSceneItemList (${sceneName}): ${JSON.stringify(e)}`, + ); + return null; + } + } + async setSceneItemEnabled(sceneName, sourceName, booleanStatus) { + let self = this; + try { + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + let sceneItemId = await self.getSceneItemId(sceneName, sourceName); + let param = { + sceneName: sceneName, + sceneItemId: sceneItemId, + sceneItemEnabled: booleanStatus, + }; + let responseTransform = await self.client.call( + "SetSceneItemEnabled", + param, + ); + // self.utils.log(`SetSceneItemEnabled (${sceneName},${sourceName},${sceneItemId}): ${booleanStatus}`); + return responseTransform; + } + } catch (e) { + self.utils.log( + `Error SetSceneItemEnabled (${sceneName},${sourceName},${sceneItemId}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async setSceneItemIdEnabled(sceneName, sceneItemId, booleanStatus) { + let self = this; + try { + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + let param = { + sceneName: sceneName, + sceneItemId: sceneItemId, + sceneItemEnabled: booleanStatus, + }; + let responseTransform = await self.client.call( + "SetSceneItemEnabled", + param, + ); + // self.utils.log(`SetSceneItemEnabled (${sceneName},${sceneItemId}): ${booleanStatus}`); + return responseTransform; + } + } catch (e) { + self.utils.log( + `Error SetSceneItemEnabled (${sceneName},${sceneItemId}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async getSceneItemIndex(sceneName, sceneItemId) { + //index 0 is bottom + let self = this; + let param = { + sceneName: sceneName, + sceneItemId: sceneItemId, + }; + try { + let response = null; + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + response = await self.client.call("GetSceneItemIndex", param); + } + //no result + self.utils.log( + `getSceneItemIndex: ${sceneName}-${sceneItemId}`, + response, + ); + return response.sceneItemId; + } catch (e) { + self.utils.log( + `Error getSceneItemIndex (${sceneName}-${sceneItemId}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + async setSceneItemIndex(sceneName, sceneItemId, sceneItemIndex) { + //index 0 is bottom + let self = this; + try { + if (self.oldWS) { + self.utils.log(`Old OBS Not Setup`); + return null; + } else { + let param = { + sceneName: sceneName, + sceneItemId: sceneItemId, + sceneItemIndex: sceneItemIndex, + }; + let response = await self.client.call("SetSceneItemIndex", param); + // self.utils.log(`setSceneItemIndex (${sceneName},${sceneItemId}): ${booleanStatus}`); + return response; + } + } catch (e) { + self.utils.log( + `Error setSceneItemIndex (${sceneName},${sceneItemId},${sceneItemIndex}): ${JSON.stringify( + e, + )}`, + ); + return null; + } + } + + //Order scenelist based on current locations. + async setSceneOrder(sceneName, order, nameFunction) { + let self = this; + try { + let result = await self.getSceneItemList(sceneName); + if (nameFunction != null && nameFunction instanceof Function) { + order.forEach((name, index) => (order[index] = nameFunction(name))); + } + if (result == null) { + return null; + } + let orderNums = []; + let sceneNames = {}; + for (let scene of result) { + let name = scene.sourceName; + let id = scene.sceneItemId; + let enabled = scene.sceneItemEnabled; + let index = scene.sceneItemIndex; + let cleanName = name; + if (nameFunction != null && nameFunction instanceof Function) { + cleanName = nameFunction(name); + } + if (order.includes(cleanName)) { + orderNums.push(index); + } + sceneNames[cleanName] = id; + } + orderNums.sort(); + let lastindex = 0; + let pos = 0; + for (let i = order.length - 1; i >= 0; i--) { + let name = order[i]; + let id = sceneNames[name]; + lastindex = orderNums[pos] ?? lastindex; + pos = pos + 1; + await self.setSceneItemIndex(sceneName, id, lastindex); + } + return true; + } catch (e) { + self.utils.log(`Error setSceneOrder (${order}): ${JSON.stringify(e)}`); + return null; + } + } +} + +/** + * Create a new OBS instance + * + * @param {keyof typeof connections} connection Connection name to create + * @returns {Promise} + */ +const create = async (connection) => { + const client = new OBS( + connections[connection].name, + connections[connection].address, + connections[connection].password, + connections[connection].old, + ); + await client.createClient(); + return client; +}; + +/** + * Establishes websocket connections to the OBS instances + * + * `controller.connections.obs.local` is the local OBS instance + * `controller.connections.obs.cloud` is the cloud OBS instance + * `controller.connections.obs.create` is a method to create new OBS instances + * + * @param {import("../controller")} controller + * @returns {Promise} + */ +module.exports = async (controller) => { + const local = await create("localAlveus"); + const cloud = await create("cloudSpace"); + + controller.connections.obs = { local, cloud, create }; +}; diff --git a/src/connections/obsbot.js b/src/connections/obsbot.js new file mode 100644 index 0000000..fe576b6 --- /dev/null +++ b/src/connections/obsbot.js @@ -0,0 +1,360 @@ +const osc = require("osc"); + +const Logger = require("../utils/logger"); + +class OBSBot { + #logger; + #client; + #connected = false; + #tracking = false; + #zoom = 0; + #fov = 0; + + /** + * Establishes a connection to an OBSBot camera + * + * @param {string} name Name of the camera (for logging) + * @param {string} host Hostname or IP address of the camera + * @param {number} port Port number of the camera + */ + constructor(name, host, port) { + this.#logger = new Logger(`connections/obsbot/${name}`); + + // Create an osc.js UDP Port listening on port. + this.#client = new osc.UDPPort({ + localAddress: "0.0.0.0", + localPort: port, + remoteAddress: host, + remotePort: port, + metadata: true, + }); + + // Listen for incoming OSC messages. + this.#client.on("message", (oscMsg, timeTag, info) => { + const address = oscMsg.address.replace("/OBSBOT/WebCam/General/", ""); + const args = oscMsg.args; // array of objects + + switch (address) { + case "ConnectedResp": + this.#connected = args[0]?.value === 1; // (1->lock; 0->unlock) + this.#logger.log( + `Connection Status: ${ + this.#connected ? "Connected" : "Disconnected" + }`, + ); + break; + case "DeviceInfo": + // this.#logger.log(`Presets: ${preset1}, ${preset2}, ${preset3}`); + break; + case "AiTrackingInfo": + this.#tracking = args[0]?.value !== 1; // (1->lock; 0->unlock) + this.#logger.log( + `AI Tracking Status: ${this.#tracking ? "Unlocked" : "Locked"}`, + ); + break; + case "ZoomInfo": + this.#zoom = parseInt(args[0]?.value) || 0; // (0~100) + this.#fov = parseInt(args[1]?.value) || 0; // 0->86°; 1->78°; 2->65° + this.#logger.log(`Zoom: ${this.#zoom}% | FOV: ${this.#fov}`); + break; + case "PresetPositionInfo": + const preset1 = args[1]?.value || "none"; + const preset2 = args[3]?.value || "none"; + const preset3 = args[5]?.value || "none"; + this.#logger.log(`Presets: ${preset1}, ${preset2}, ${preset3}`); + break; + default: + this.#logger.log("Unhandled OSC Message:", oscMsg); + } + }); + + this.#client.on("error", (err) => { + this.#logger.error("OBSBot Server ERROR", err); + }); + + // When the port is read, send an OSC message to, say, SuperCollider + this.#client.on("ready", () => { + this.#logger.log("OBSBot Server Ready"); + this.getConnected(); + }); + + this.connect(); + } + + /** + * Send a command to the camera + * + * @param {string} command Command to send + * @param {number} value Value to send with the command + * @private + */ + #send(command, value) { + this.#client.send({ + address: command, + args: [ + { + type: "i", + value: value, + }, + ], + }); + } + + /** + * Connects to the camera + * + * This is automatically called when the class is instantiated + */ + connect() { + this.#client.open(); + } + + /** + * Disconnects from the camera + */ + disconnect() { + this.#client.close(); + } + + /** + * Request the connection status from the camera + */ + getConnected() { + this.#send("/OBSBOT/WebCam/General/Connected", 0); + } + + /** + * Request device information from the camera + */ + getInfo() { + this.#send("/OBSBOT/WebCam/General/GetDeviceInfo", 0); + } + + /** + * Request zoom information from the camera + */ + getZoomInfo() { + this.#send("/OBSBOT/WebCam/General/GetZoomInfo", 0); + } + + /** + * Request tracking information from the camera + */ + getTrackingInfo() { + this.#send("/OBSBOT/WebCam/General/GetAiTrackingInfo", 0); + } + + /** + * Request preset position information from the camera + */ + getPresetPosition() { + this.#send("/OBSBOT/WebCam/General/GetPresetPositionInfo", 0); + } + + /** + * Apply a preset position to the camera + * + * @param {number} preset Preset position to apply (1-3) + */ + setPreset(preset) { + const parsed = parseInt(preset); + if (isNaN(parsed) || parsed < 1 || parsed > 3) return; // TODO: Throw error? + + // 0->Preset Position 1;1->Preset Position 2;2->Preset Position 3 + this.#send("/OBSBOT/WebCam/General/TriggerPreset", parsed - 1); + this.#logger.log(`Setting Preset: ${parsed - 1}`); + } + + /** + * Wake the camera from sleep + */ + wake() { + this.#send("/OBSBOT/WebCam/General/WakeSleep", 1); + this.#logger.log("Awaking Device"); + } + + /** + * Put the camera to sleep + */ + sleep() { + this.#send("/OBSBOT/WebCam/General/WakeSleep", 0); + this.#logger.log("Sleeping Device"); + } + + /** + * Lock/disable tracking on the camera + */ + lockTracking() { + this.#send("/OBSBOT/WebCam/General/ToggleAILock", 1); + this.#logger.log("Locked Tracking"); + } + + /** + * Unlock/enable tracking on the camera + */ + unlockTracking() { + this.#send("/OBSBOT/WebCam/General/ToggleAILock", 0); + this.#logger.log("Unlocked Tracking"); + } + + /** + * Enable/disable tracking on the camera + * + * @param {0|"0"|"off"|"no"|1|"1"|"on"|"yes"} value Value to set + */ + setTracking(value) { + const parsed = + value === "1" || value === "on" || value === "yes" + ? 1 + : value === "0" || value === "off" || value === "no" + ? 0 + : value; + if (parsed !== 0 && parsed !== 1) return; // TODO: Throw error? + + // 1->Target lock; 0->Target unlock + this.#send("/OBSBOT/WebCam/General/ToggleAILock", parsed); + this.#logger.log(`Setting Tracking: ${parsed ? "Locked" : "Unlocked"}`); + } + + /** + * Set the zoom level on the camera + * + * @param {number|string} zoom Zoom level to set (0-100) + */ + setZoom(zoom) { + const parsed = parseInt(zoom); + if (isNaN(parsed) || parsed < 0 || parsed > 100) return; // TODO: Throw error? + + // percentage 0-100, each number corresponds to a zoom value of 1%. + this.#send("/OBSBOT/WebCam/General/SetZoom", parsed); + this.#logger.log(`Setting Zoom: ${parsed}%`); + } + + /** + * Set the FOV mode on the camera + * + * @param {number|string} fov FOV mode to apply (1-3) + */ + setFOV(fov) { + const parsed = parseInt(fov); + if (isNaN(parsed) || parsed < 1 || parsed > 3) return; // TODO: Throw error? + + // 0->86°;1->78°;2->65° + this.#send("/OBSBOT/WebCam/General/SetView", parsed - 1); + this.#logger.log(`Setting FOV: ${parsed - 1}`); + } + + /** + * Reset the camera's position + */ + resetPosition() { + this.#send("/OBSBOT/WebCam/General/ResetGimbal", 0); + this.#logger.log("Resetting Gimbal Position"); + } + + /** + * Tilt the camera by a relative amount + * + * Note that movement will be approximate as the movement is done based on time, not the actual position of the camera + * + * @param {number|string} value Distance to tilt + */ + tilt(value) { + const parsed = parseInt(value); + if (isNaN(parsed) || parsed === 0) return; // TODO: Throw error? + + // It takes ~7secs to fully tilt + const time = Math.min(Math.abs(parsed) * (5600 / 180), 7500); + + if (parsed > 0) this.gimbalUp(); + else this.gimbalDown(); + + setTimeout(this.stop.bind(this), time); + } + + /** + * Pan the camera by a relative amount + * + * Note that movement will be approximate as the movement is done based on time, not the actual position of the camera + * + * @param {number|string} value Distance to pan + */ + pan(value) { + const parsed = parseInt(value); + if (isNaN(parsed) || parsed === 0) return; // TODO: Throw error? + + // It takes ~10secs to fully pan + const time = Math.min(Math.abs(parsed) * (9000 / 340), 11000); + + if (parsed > 0) this.gimbalRight(); + else this.gimbalLeft(); + + setTimeout(this.stop.bind(this), time); + } + + /** + * Stop the camera's movement + */ + stop() { + this.#send("/OBSBOT/WebCam/General/SetGimbalUp", 0); + this.#logger.log("Stopping Gimbal"); + } + + /** + * Move the camera up + * + * Movement will continue until `stop()` is called + */ + gimbalUp() { + this.#send("/OBSBOT/WebCam/General/SetGimbalUp", 1); + this.#logger.log("Moving Gimbal Up"); + } + + /** + * Move the camera down + * + * Movement will continue until `stop()` is called + */ + gimbalDown() { + this.#send("/OBSBOT/WebCam/General/SetGimbalDown", 1); + this.#logger.log("Moving Gimbal Down"); + } + + /** + * Move the camera left + * + * Movement will continue until `stop()` is called + */ + gimbalLeft() { + this.#send("/OBSBOT/WebCam/General/SetGimbalLeft", 1); + this.#logger.log("Moving Gimbal Left"); + } + + /** + * Move the camera right + * + * Movement will continue until `stop()` is called + */ + gimbalRight() { + this.#send("/OBSBOT/WebCam/General/SetGimbalRight", 1); + this.#logger.log("Moving Gimbal Right"); + } +} + +/** + * Establishes connections to the OBSBot camera + * + * `controller.connections.obsBot` is the OBSBot connection + * + * @param {import("../controller")} controller + * @returns {Promise} + */ +module.exports = async (controller) => { + const obsBot = new OBSBot( + "NutHouse", + process.env.OBSBOT_HOST, + process.env.OBSBOT_PORT, + ); + + controller.connections.obsBot = obsBot; +}; diff --git a/src/connections/twitch.js b/src/connections/twitch.js new file mode 100644 index 0000000..d71213e --- /dev/null +++ b/src/connections/twitch.js @@ -0,0 +1,295 @@ +const { join } = require("node:path"); +const { readFileSync, writeFileSync } = require("node:fs"); + +const { ApiClient } = require("@twurple/api"); +const { ChatClient } = require("@twurple/chat"); +const { RefreshingAuthProvider } = require("@twurple/auth"); + +const Logger = require("../utils/logger"); +const config = require("../config/config"); + +class Twitch { + #logger; + #auth; + #client; + #chat; + #channels; + #level = "WARNING"; // CRITICAL, ERROR, WARNING, INFO, DEBUG = 4, TRACE = 7 + + /** + * Create a new Twitch connection. + * + * @param {string} name Name of the account (for logging) + * @param {string} id Client ID for authentication + * @param {string} secret Client secret for authentication + * @param {string} file File location to read/write token data + * @param {string[]} channels List of channels to join + * @returns {Promise} + */ + constructor(name, id, secret, file, channels) { + this.#logger = new Logger(`connections/twitch/${name}`); + + // Authenticate and create the client + this.#auth = new RefreshingAuthProvider( + { + clientId: id, + clientSecret: secret, + onRefresh: (data) => { + this.#logger.log("Refreshing Twitch token"); + writeFileSync(file, JSON.stringify(data, null, 4), "UTF-8"); + }, + }, + JSON.parse(readFileSync(file, "UTF-8")), // "./tokens.json" + ); + this.#client = new ApiClient({ + authProvider: this.#auth, + logger: { + minLevel: this.#level, + }, + }); + + // Connect to chat in the specified channels + if ( + !this.#auth.currentScopes.includes("chat:read") || + !this.#auth.currentScopes.includes("chat:edit") + ) { + throw new Error(`Twitch client (${name}) is missing chat scopes`); + } + this.#channels = new Set(channels); + this.#chat = new ChatClient({ + authProvider: this.#auth, + channels: [...this.#channels], + logger: { + minLevel: this.#level, + }, + }); + + // Register standard chat events + this.#chat.onRegister(() => { + this.#logger.log(`Chat connected (${[...this.#channels].join(", ")})`); + }); + this.#chat.onJoin((channel, user) => { + this.#logger.log(`${user} joined ${channel.substring(1)}`); + this.#channels.add(channel.substring(1)); + }); + this.#chat.onJoinFailure((channel, reason) => { + this.#logger.warn(`Failed to join ${channel.substring(1)}: ${reason}`); + }); + this.#chat.onPart((channel, user) => { + this.#logger.log(`${user} parted ${channel.substring(1)}`); + this.#channels.delete(channel.substring(1)); + }); + this.#chat.onMessageFailed((channel, reason) => { + this.#logger.warn( + `Failed to send message (${channel.substring(1)}): ${reason}`, + ); + }); + this.#chat.onMessageRatelimit((channel, reason) => { + this.#logger.warn( + `Chat message ratelimited (${channel.substring(1)}): ${reason}`, + ); + }); + this.#chat.onNoPermission((channel, message) => { + this.#logger.warn( + `No permission for chat action (${channel.substring(1)}): ${message}`, + ); + }); + this.#chat.onAuthenticationFailure((message) => { + this.#logger.warn(`Authentication failure: ${message}`); + }); + this.#chat.onDisconnect((manually, reason) => { + if (manually) + this.#logger.log(`Disconnected from chat manually: ${reason}`); + else this.#logger.warn(`Disconnected from chat: ${reason}`); + }); + + // Connect to Twitch + return this.#chat.connect().then(() => this); + } + + /** + * Register a callback for chat messages. + * + * @param {function(string, string, string, Object): void} func Callback function (channel, user, message, msg) + */ + onMessage(func) { + this.#chat.onMessage((channel, user, message, msg) => + func(channel.substring(1).toLowerCase(), user, message.trim(), msg), + ); + } + + /** + * Join a channel. + * + * @param {string} channel Channel to join + * @returns {Promise} List of connected channels + */ + async join(channel) { + try { + await this.#chat.join(channel); + return [...this.#channels]; + } catch (e) { + this.#logger.error(`Failed to join chat channel (${channel}): ${e}`); + return null; + } + } + + /** + * Leave a channel. + * + * @param {string} channel Channel to leave + * @returns {Promise} List of connected channels + */ + leave(channel) { + try { + this.#chat.part(channel); + return [...this.#channels]; + } catch (e) { + this.#logger.error(`Failed to leave chat channel (${channel}): ${e}`); + return null; + } + } + + /** + * Send a message to a channel. + * + * @param {string} channel Channel to send message to + * @param {string} message Message to send + * @returns {Promise} Whether the message was sent successfully + */ + async send(channel, message) { + try { + let messageList = []; + if (message.length > 500){ + let splitString = message.match(/.{1,480}([\.\s,]|$)/g).map(item => item.trim()); + const remaining = message.replace(splitString.join(''), ''); + messageList = [...splitString, remaining.trim()] + } else { + messageList = [message]; + } + for (let m of messageList){ + await this.#chat.say(channel, m); + } + return true; + } catch (e) { + this.#logger.error(`Failed to send chat message (${channel}): ${e}`); + return false; + } + } + + /** + * Get stream information for a user/channel. + * + * @param {string | number} id ID of the user/channel + * @returns {Promise} Channel information + */ + async getStreamInfo(id) { + try { + // delay, displayName, gameId, gameName, id, name, title + return await this.#client.channels.getChannelInfoById(id); + } catch (e) { + this.#logger.error(`Failed to get stream info (${id}): ${e}`); + return null; + } + } + + /** + * Set stream information for a user/channel. + * + * @param {string | number} id ID of the user/channel + * @param {string} title Stream title + * @param {number | "animals" | "chatting" | "pools"} game ID of the game (or "animals", "chatting", "pools") + * @returns {Promise} Whether the stream info was set successfully + */ + async setStreamInfo(id, title, game) { + try { + const aliases = { + animals: 272263131, + chatting: 509658, + pools: 116747788, + }; + game = aliases[game] || game; + + this.#logger.log(`Setting stream info (${id}): ${title} - ${game}`); + await this.#client.channels.updateChannelInfo(id, { + title, + gameId: game, + }); + return true; + } catch (e) { + this.#logger.error(`Failed to set stream info (${id}): ${e}`); + return false; + } + } + + /** + * Run a commercial on a user/channel. + * + * @param {string | number} id ID of the user/channel + * @param {30 | 60 | 90 | 120 | 150 | 180} length Length of the commercial + * @returns {Promise} Whether the commercial was run successfully + */ + async runCommercial(id, length) { + if (![30, 60, 90, 120, 150, 180].includes(length)) { + this.#logger.warn(`Invalid commercial length (${id}): ${length}`); + return false; + } + + try { + await this.#client.channels.startChannelCommercial(id, length); + return true; + } catch (e) { + this.#logger.error(`Failed to run commercial (${id}): ${e}`); + return false; + } + } + + /** + * Create a stream marker for a user/channel. + * + * @param {string | number} id ID of the user/channel + * @param {string} description Description of the marker + * @returns {Promise} Marker information + */ + async createMarker(id, description) { + try { + // creationDate, description, id, positionInSeconds + return await this.#client.streams.createStreamMarker(id, description); + } catch (e) { + this.#logger.error(`Failed to create stream marker (${id}): ${e}`); + return null; + } + } +} + +/** + * Establishes connections to the Twitch API + * + * `controller.connections.twitch` is the Twitch instance + * + * @param {import("../controller")} controller + * @returns {Promise} + */ +module.exports = async (controller) => { + const logger = new Logger("connections/twitch"); + + if ( + !process.env.ALVEUS_CLIENT_ID || + !process.env.ALVEUS_CLIENT_SECRET || + !process.env.ALVEUS_TOKEN_PATH + ) { + logger.warn( + "No Twitch API credentials found. Twitch connections will not be established. To enable Twitch connections, set the ALVEUS_CLIENT_ID, ALVEUS_CLIENT_SECRET, and ALVEUS_TOKEN_PATH environment variables.", + ); + return; + } + + const twitch = await new Twitch( + "AlveusSanctuary", + process.env.ALVEUS_CLIENT_ID, + process.env.ALVEUS_CLIENT_SECRET, + join(process.cwd(), process.env.ALVEUS_TOKEN_PATH), + config.twitchChannelList, + ); + controller.connections.twitch = twitch; +}; diff --git a/src/connections/unifi.js b/src/connections/unifi.js new file mode 100644 index 0000000..f233309 --- /dev/null +++ b/src/connections/unifi.js @@ -0,0 +1,358 @@ +const { Controller } = require("unifi-client"); + +const Logger = require("../utils/logger"); + +const logger = new Logger("connections/unifi"); + +class Unifi { + /** + * Regex to check valid MAC address + * + * @type {RegExp} + */ + static #reMac = new RegExp( + /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})|([0-9a-fA-F]{4}.[0-9a-fA-F]{4}.[0-9a-fA-F]{4})$/, + ); + + /** @type {import("unifi-client").Site} */ + #site; + + /** + * Override data as injected by addOverrides + * + * @typedef {{ mac: string, name: string, client_name: string, override: true }} BaseOverriddenClient + */ + + /** + * Full client data from Unifi, for an overridden client + * + * @typedef {import("unifi-client").Client & BaseOverriddenClient} OverriddenClient + */ + + /** + * Full client data from Unifi, for a standard client + * + * @typedef {import("unifi-client").Client & { client_name: string }} StandardClient + */ + + /** @type {Record} */ + #clients = {}; + + /** + * Establishes a connection to a Unifi Console + * + * @param {string} host Hostname or IP address of the camera + * @param {string} username Username to use for authentication + * @param {string} password Password to use for authentication + * @param {Record} [overrides={}] Mapping of MAC addresses to names (to override names from Unifi) + * @returns {Promise} + */ + constructor(host, username, password, overrides = {}) { + // Create the controller + const controller = new Controller({ + username: username, + password: password, + url: "https://" + host.replace(/https?:\/\//i, ""), + strictSSL: false, + }); + + // Add any initial overrides + this.addOverrides(overrides); + + // Start the connection + return controller.login().then(async () => { + // Get the default site + const sites = await controller.getSites(); + this.#site = sites.find((s) => s.name === "default") || sites[0]; + if (!this.#site) throw new Error("No sites found"); + + // Populate the full client data, and log how many we found + const clients = await this.getClients(); + logger.log(`Connected. ${Object.keys(clients).length} clients`); + + // Return the instance + return this; + }); + } + + /** + * Override the names of clients by MAC address + * + * @param {Record} overrides Mapping of MAC addresses to names (to override names from Unifi) + */ + addOverrides(overrides) { + for (const [mac, name] of Object.entries(overrides)) { + this.#clients[mac] = { + // Merge with any existing data for this client + ...this.#clients[mac], + + // Set the mac address and name, marking it as an override + mac, + name, + client_name: name.toLowerCase().replaceAll(" ", ""), + override: true, + }; + } + } + + /** + * Get all clients from Unifi + * + * @returns {Promise>>} + */ + async getClients() { + // Get all the clients from Unifi + const clients = await this.#site.clients.list(); + for (const client of clients) { + // Get the MAC address and name + const mac = client.mac.toLowerCase(); + const name = (client.name || client.hostname || mac) + .toLowerCase() + .replaceAll(" ", ""); + + // Clone and store the client data + this.#clients[mac] = { + ...client, + + // Apply any overrides, and use the cleaned up name + name: this.#clients[mac]?.override + ? this.#clients[mac].name + : client.name, + client_name: this.#clients[mac]?.override + ? this.#clients[mac].client_name + : name, + }; + } + + // Return a deep-frozen copy of the clients + return Object.freeze( + Object.fromEntries( + Object.entries(this.#clients).map(([mac, client]) => [ + mac, + Object.freeze(client), + ]), + ), + ); + } + + /** + * Get a specific client by MAC address + * + * @param {string} mac MAC address of the client to get + * @returns {Promise | null>} + */ + async getClientByMac(mac) { + // Fetch the raw device from Unifi + // TODO: Should this check that `mac` is a valid MAC address? + const response = await this.#site + .getInstance() + .get(`/stat/sta/${mac.toLowerCase()}`) + .catch((e) => { + logger.error(`Failed to get client ${mac}: ${e}`); + return null; + }); + + // Extract the returned clients, and find the matching client + // TODO: What does the data structure look like? Is it StandardClient? Code in getSignal would suggest `ap_mac` is a thing, which isn't in StandardClient + const client = response?.data?.data?.find((d) => d.mac === mac); + if (!client) { + logger.log(`Failed to find client ${mac}`); + return null; + } + + // Set the client_name using the cleaned up name + // TODO: Abstract this, same logic in getClients (and partially addOverrides) + const name = (client.name || client.hostname || mac) + .toLowerCase() + .replaceAll(" ", ""); + logger.log(`Found client ${name} (${mac})`); + + // Return a frozen copy of the client + // TODO: Should this apply overrides? + return Object.freeze({ + ...client, + client_name: name, + }); + } + + /** + * Get clients matching a given name + * + * @param {string} name Client name to look up + * @returns {Promise>, null>[]>>} + */ + async getClientsByName(name) { + return ( + Promise.all( + // Convert the name to matching MAC addresses + this.#convertNameToMacs(name) + // And get the full client data for each MAC address + .map((mac) => this.getClientByMac(mac)), + ) + // Filter out any MAC addresses that didn't match a client + .then((clients) => clients.filter(Boolean)) + ); + } + + /** + * Get signal data for a given client name + * + * @param {string} name Client name to look up + * @returns {Promise | null>} + */ + async getSignal(name) { + // Convert the name to a matching client with a signal + const client = await this.#convertNameToClient(name); + if (!client) { + logger.log(`Failed to find client ${name} with signal`); + return null; + } + + // Extract some core signal and AP data from the client + const hostname = client.client_name; + const signal = client.signal; + const client_mac = client.mac; + const ap_mac = client.ap_mac; + const ap_name = this.#convertMacToName(ap_mac); + logger.log( + `Client ${hostname} is connected to ${ap_name} (${ap_mac}) with signal ${signal}`, + ); + return { + hostname, + signal, + client_mac, + ap_mac, + ap_name, + name, + }; + } + + /** + * Force a client to reconnect by MAC address + * + * @param {string} mac MAC address of the client to reconnect + * @returns {Promise} + */ + async clientReconnectByMac(mac) { + const response = await this.#site + .getInstance() + .post("/cmd/stamgr", { + cmd: "kick-sta", + mac: mac.toLowerCase(), + }) + .catch((e) => { + logger.error(`Failed to reconnect ${mac}: ${e}`); + return null; + }); + + // If we got an ok response code, return true + const data = response?.data?.meta; + if (data?.rc === "ok") { + logger.log(`Reconnected ${mac}`); + return true; + } + + // Otherwise, log the error and return the response + logger.log(`Failed to reconnect ${mac}: ${data?.rc}, ${data?.msg}`); + return false; + } + + /** + * Force a client to reconnect by name + * + * @param {string} name Client name to look up and reconnect + * @returns {Promise} + */ + async clientReconnect(name) { + // Convert the name to a matching client with a signal + const client = await this.#convertNameToClient(name); + if (!client) { + logger.log(`Failed to find client ${name} to reconnect`); + return false; + } + + // Attempt to reconnect the client + return this.clientReconnectByMac(client.mac); + } + + /** + * Convert a client name to a client object + * + * @param {string} name Client name to look up + * @returns {ReturnType} + * @private + */ + async #convertNameToClient(name) { + // Convert the name to matching MAC addresses + // and, iterate through them until we find a client with a signal + for (const mac of this.#convertNameToMacs(name)) { + const client = await this.getClientByMac(mac); + if (client?.signal && client?.client_name) return client; + } + + // If we didn't find a client with a signal, return null + return null; + } + + /** + * Convert a client name to a list of matching MAC addresses + * + * @param {string} name Client name to look up + * @returns {string[]} List of matching MAC addresses + * @private + */ + #convertNameToMacs(name) { + // If we were given a valid MAC address, just return it + if (this.isValidMacAddress(name)) return [name]; + + // Find all the matching clients, + // sort them by most recently seen, + // and return their MAC addresses + const search = (name || "").toLowerCase().replaceAll(" ", ""); + return Object.values(this.#clients) + .filter((client) => client.client_name === search) + .sort((a, b) => new Date(b.lastSeen) - new Date(a.lastSeen)) + .map((client) => client.mac); + } + + /** + * Convert a MAC address to a client name + * + * @param {string} mac MAC address to look up + * @returns {string} Matching client name, or MAC address if no name is found + * @private + */ + #convertMacToName(mac) { + return this.#clients[mac]?.client_name || mac; + } + + /** + * Check if a given value is a valid MAC address + * + * @param {string} str Value to check + * @returns {boolean} + */ + isValidMacAddress(str) { + return str && typeof str === "string" && Unifi.#reMac.test(str); + } +} + +/** + * Establishes connections to Unifi Console + * + * `controller.connections.unifi` is the Unifi connection + * + * @param {import("../controller")} controller + * @returns {Promise} + */ +module.exports = async (controller) => { + controller.connections.unifi = {}; + return; + const unifi = new Unifi( + process.env.UNIFI_IP, + process.env.UNIFI_USERNAME, + process.env.UNIFI_PASSWORD, + JSON.parse(process.env.UNIFI_AP_MACS) || {}, + ); + controller.connections.unifi = unifi; +}; diff --git a/src/controller.js b/src/controller.js new file mode 100644 index 0000000..22f7b37 --- /dev/null +++ b/src/controller.js @@ -0,0 +1,44 @@ +const { readdir } = require("node:fs/promises"); +const { resolve, relative } = require("node:path"); + +const Logger = require("./utils/logger"); + +/** + * Get all files in a directory recursively + * + * @param {string} dir + * @returns {Promise} + */ +const getAllFiles = (dir) => + readdir(dir, { withFileTypes: true }).then((dirents) => + Promise.all( + dirents.map((dirent) => { + const res = resolve(dir, dirent.name); + return dirent.isDirectory() ? getAllFiles(res) : res; + }), + ).then((files) => files.flat()), + ); + +class Controller { + #connections = {}; + #logger = new Logger("controller"); + + get connections() { + return this.#connections; + } + + async load(directory) { + const files = await getAllFiles(directory); + for (const file of files) { + if (!file.endsWith(".js")) continue; + + const name = relative(process.cwd(), file).slice(0, -3); + this.#logger.log(`Loading ${name}...`); + + const connection = require(file); + if (typeof connection === "function") await connection(this); + } + } +} + +module.exports = Controller; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..0693cdf --- /dev/null +++ b/src/index.js @@ -0,0 +1,31 @@ +const { join } = require("node:path"); + +try { + process.chdir('./src'); +} +catch (err) { + console.log('chdir: ' + err); +} + +const Controller = require("./controller"); + +// Load any ENV variables from .env file +const envFile = process.env.NODE_ENV == 'development' ? `.env.development.local` : '.env'; +require('dotenv').config({ path: join(process.cwd(), envFile) }); + + +const main = async () => { + // Create our controller object + const controller = new Controller(); + + // Get all our connections + await controller.load("./connections"); + + // Get all our modules + await controller.load("./modules"); +}; + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/src/modules/legacy.js b/src/modules/legacy.js new file mode 100644 index 0000000..4c8dc09 --- /dev/null +++ b/src/modules/legacy.js @@ -0,0 +1,2251 @@ +const UtilsModule = require("../utils/utilsModule"); +const config = require("../config/config"); +const helper = require("../utils/helper"); +const Logger = require("../utils/logger"); + +const logger = new Logger("modules/legacy"); + +let roamTimeout = null; +/** + * Legacy controller implementation, handling scene changes and Twitch chat commands + * + * @param {import("../controller")} controller + */ +const main = async controller => { + // Bind event handlers + if (controller.connections.obs?.local) { + controller.connections.obs.local.sceneChange(onSceneChange.bind(null, controller)); + } else { + logger.warn("Local OBS connection not found. Scene changes will not be handled."); + } + + if (controller.connections.obs?.cloud) { + controller.connections.obs.cloud.sceneChange(onSceneChangeCloud.bind(null, controller)); + } else { + logger.warn("Cloud OBS connection not found. Scene changes will not be handled."); + } + + if (controller.connections.twitch) { + controller.connections.twitch.onMessage(onTwitchMessage.bind(null, controller)); + } else { + logger.warn("Twitch connection not found. Twitch chat messages will not be handled."); + } + + // Log the current scene in OBS + const currentScene = helper.cleanName(controller.connections.obs?.local?.currentScene || ""); + logger.log("Starting up... Current scene:", currentScene); + + // Restore PTZ roaming status + if (controller.connections.database?.[currentScene]?.isRoaming) { + clearTimeout(roamTimeout); + setPTZRoamMode(controller, currentScene); + } + + runAtSpecificTimeOfDay(7, 55, () => { + try { + logger.log(`Timer (7:55am) - Send !nightcams !mute fox`); + controller.connections.twitch.send("alveusgg", `!nightcams`); + controller.connections.obs.local.setMute(config.sceneAudioSource["fox"], true); + switchToCustomCams(controller, "alveusgg", { allowed: true, accessLevel: 'commandAdmins' }, "customcamsbig", config.customCamCommandMapping["nightcams"]); + } catch (e) { + logger.log(`Error: Failed to run timer - ${e}`); + } + }); + + for (let hour = 1; hour < 5; hour++) { + for (let min = 0; min < 60; min += 10) { + runAtSpecificTimeOfDay(hour, min, () => { + try { + logger.log(`Running Commerical ${hour}:${min}`); + controller.connections.twitch.runCommercial(config.alveusTwitchID, 180); + } catch (e) { + logger.log(`Error: Failed to run commerical - ${e}`); + } + }); + } + } +} + +module.exports = main; + +/** + * Handle local OBS scene changes + * + * @param {import("../controller")} controller + * @param {string} name + * @param {string} oldName + * @returns {Promise} + */ +const onSceneChange = async (controller, name, oldName) => { + logger.log(`Scene Change from ${oldName} to ${name}`); + + let newScene = name ?? ""; + newScene = newScene.replaceAll(" ", ""); + newScene = newScene.toLowerCase(); + + let oldScene = oldName ?? ""; + oldScene = oldScene.replaceAll(" ", ""); + oldScene = oldScene.toLowerCase(); + + //do nothing if not live, cloud server not live, in studio mode, cloud server not on Alveus Server + let processChange = false; + let cloudLive = await controller.connections.obs.cloud.isLive() || false; + if (cloudLive) { + let currentCloudScene = await controller.connections.obs.cloud.getScene() || ""; + //logger.log("localserver change, currentcloud: ",currentCloudScene) + if (currentCloudScene == "Alveus Server") { + let localLive = await controller.connections.obs.local.isLive() || false; + if (localLive) { + let localStudioMode = await controller.connections.obs.local.isStudioMode() || false; + if (!localStudioMode) { + processChange = true; + } + } + } + } + if (!processChange) { + //do nothing + return; + } + + //Clear CustomCam list if changed + // if (oldScene == "customcams") { + // clearCustomCamsDB(controller); + // } + + //Mute Global music if changing to any other scene + if (newScene != "customcams") { + //controller.connections.obs.local.setMute(config.globalMusicSource, true); + } + + if (!config.pauseNotify) { + let now = new Date(); + // var minutes = now.getMinutes(); + var hour = now.getHours(); + //logger.log("scene change current time",hour,now); + //if ((hour >= 19 && minutes >= 30) || hour >= 20 || hour < 8){ + if ( hour >= config.notifyHours.start && hour < config.notifyHours.end ) { + let continueCheck = true; + + if (newScene == "customcam" || oldScene == "customcam") { + //don't notify + continueCheck = false; + } + + let onewayList = config.onewayNotifications[oldScene]; + if (onewayList != null) { + //check if leaving multiscene, allow child scenes. + for (let i = 0; i < onewayList.length; i++) { + let sceneName = onewayList[i] || ""; + sceneName = helper.cleanName(sceneName); + if (newScene == sceneName) { + //don't notify + continueCheck = false; + } + } + } + if (continueCheck) { + logger.log("Notify: Not oneway"); + //Multiscene disable notification + let newSceneIsMulti = false; + let oldSceneIsMulti = false; + for (let multiScene in config.multiScenes) { + let sceneList = config.multiScenes[multiScene]; + //check if this is a multiscene and find parent camera + for (let i = 0; i < sceneList.length; i++) { + let sceneName = sceneList[i] || ""; + sceneName = helper.cleanName(sceneName); + if (newScene == sceneName) { + //found match + newSceneIsMulti = true; + } else if (oldScene == sceneName) { + //found match + oldSceneIsMulti = true; + } + } + } + if (newSceneIsMulti && oldSceneIsMulti) { + //if both scenes are part of multiscene, do not notify + continueCheck = false; + } + } + + //keep checking if not multiscene + if (continueCheck) { + logger.log("Notify: Not multiScenes", config.notifyScenes, newScene, oldScene); + let foundNewName = false; + let foundOldName = false; + for (let i = 0; i < config.notifyScenes.length; i++) { + let notifySceneName = config.notifyScenes[i] ?? ""; + notifySceneName = notifySceneName.replaceAll(" ", ""); + notifySceneName = notifySceneName.toLowerCase(); + if (newScene == notifySceneName) { + foundNewName = true; + break; + } else if (oldScene == notifySceneName) { + foundOldName = true; + break; + } + } + let timestamp = new Date().toLocaleString("en-US", { timeZone: "America/Chicago" }); + if (foundNewName) { + //notify its on + logger.log("Sending Notification newName. From: ", oldName, "to", name); + controller.connections.courier.sendListNotificationDiscord("livecamalert", "LivecamAlerts", `${name} Active`, `Livecam Switched to ${name}`, [{ name: "Time", value: timestamp }]); + } else if (foundOldName) { + //notify its off + logger.log("Sending Notification Oldname. From: ", oldName, "to", name); + controller.connections.courier.sendListNotificationDiscord("livecamalert", "LivecamAlerts", `${oldName} No Longer Active`, `Livecam Switched to ${name}`, [{ name: "Time", value: timestamp }]); + } + } + } + } + + if (!config.pauseGameChange) { + let justChattingCams = ["Backpack", "Backpack Server", "Alveus PC", "Nuthouse"]; + let poolsCam = []; //["Georgie"]; + + //Alveus Ambassador 24/7 Live Cam | NEW Holiday Sweater !merch | !alveus !hat !vid + //lastStreamInfo = await controller.connections.twitch.getStreamInfo(config.alveusTwitchID); + //logger.log("Twitch Info: ",lastStreamInfo.title," game: ",lastStreamInfo.gameName," gameid: ",lastStreamInfo.gameId); + + if (justChattingCams.includes(name)) { + await controller.connections.twitch.setStreamInfo(config.alveusTwitchID, null, "chatting"); + } else if (poolsCam.includes(name)) { + await controller.connections.twitch.setStreamInfo(config.alveusTwitchID, null, "pools"); + } else { + await controller.connections.twitch.setStreamInfo(config.alveusTwitchID, null, "animals"); + } + } + + //Create Twitch Vod Markers + if (!config.pauseTwitchMarker) { + let markerCams = ["Nuthouse"]; //"Backpack", "Backpack Server", "Alveus PC", + + if (markerCams.includes(name)) { + //new swap + let marker = await controller.connections.twitch.createMarker(config.alveusTwitchID, `Livecam Switched to ${name}`); + logger.log("Marker Created: ", marker.creationDate, marker.description, marker.id, marker.positionInSeconds); + } else if (markerCams.includes(oldName)) { + //ending swap + let marker = await controller.connections.twitch.createMarker(config.alveusTwitchID, `Livecam Switched to ${name}`); + logger.log("Marker Created: ", marker.creationDate, marker.description, marker.id, marker.positionInSeconds); + } + } + + //Announce Alveus Server Changes to Twitch Chat + if (config.announceChatSceneChange) { + let message = `Scene Changed to ${name}`; + controller.connections.twitch.send("alveussanctuary", message); + } +} + +/** + * Handle cloud OBS scene changes + * + * @param {import("../controller")} controller + * @param {string} name + * @param {string} oldName + * @returns {Promise} + */ +const onSceneChangeCloud = async (controller, name, oldName) => { + logger.log(`Cloud Scene Change from ${oldName} to ${name}`); + + //do nothing if not live, cloud server not live, in studio mode, cloud server not on Alveus Server + let processChange = false; + let cloudLive = await controller.connections.obs.cloud.isLive() || false; + if (cloudLive) { + let cloudStudioMode = await controller.connections.obs.cloud.isStudioMode() || false; + if (!cloudStudioMode) { + if (name != "Alveus Server") { + processChange = true; + } + } + } + if (!processChange) { + //do nothing + return; + } + + if (!config.pauseGameChange) { + + //Alveus Ambassador 24/7 Live Cam | NEW Holiday Sweater !merch | !alveus !hat !vid + //lastStreamInfo = await controller.connections.twitch.getStreamInfo(config.alveusTwitchID); + //logger.log("Twitch Info: ",lastStreamInfo.title," game: ",lastStreamInfo.gameName," gameid: ",lastStreamInfo.gameId); + + // let notJustChatting = []; + // if (notJustChatting.includes(name)) { + // // await controller.connections.twitch.setStreamInfo(config.alveusTwitchID,null,"Just Chatting"); + // } else { + // await controller.connections.twitch.setStreamInfo(config.alveusTwitchID, null, "Just Chatting"); + // } + } + + if (!config.pauseTwitchMarker) { + let markerCams = ["RTMP_Source", "Maya LiveU", "Alveus PC"]; + + if (markerCams.includes(name)) { + //new swap + let marker = await controller.connections.twitch.createMarker(config.alveusTwitchID, `Livecam Switched to ${name}`); + logger.log("Marker Created: ", marker.creationDate, marker.description, marker.id, marker.positionInSeconds); + } else if (markerCams.includes(oldName)) { + //ending swap + let marker = await controller.connections.twitch.createMarker(config.alveusTwitchID, `Livecam Switched to ${name}`); + logger.log("Marker Created: ", marker.creationDate, marker.description, marker.id, marker.positionInSeconds); + } + } + + //Announce Alveus Server Changes to Twitch Chat + if (config.announceChatSceneChange) { + let message = `Cloud Scene Changed to ${name}`; + controller.connections.twitch.send("alveussanctuary", message); + } +} + +/** + * Handle incoming Twitch chat messages + * + * @param {import("../controller")} controller + * @param {string} channel + * @param {Object} user + * @param {string} message + * @param {Object} tags + * @returns {Promise} + */ +const onTwitchMessage = async (controller, channel, user, message, tags) => { + message = message.trim(); + + // logger.log("Message",message); + + //check if blacklisted from commands + if (config.userBlacklist.includes(user.toLowerCase())) { + return; + } + + //check if command and return without prefix + let userCommand = helper.commandCheck(message); + if (userCommand == null) { + //not a command + return; + } + + // logger.log("userCommand", userCommand) + + let accessProfile = helper.isAllowed(userCommand, tags.userInfo); + if (accessProfile == null || !accessProfile.allowed) { + //no permission + // logger.log("NOT valid user",user,userCommand); + return; + } + //logger.log("valid user",user,userCommand,config.commandScenes[userCommand],accessProfile); + + // //check Throttled + // if (config.throttledCommands.includes(userCommand)){ + // let now = new Date(); + // let before = timeSinceThrottled[userCommand]; + // let differenceMS = now.getTime() - timeSinceThrottled.getTime(); + // if (differenceMS < config.throttleCommandLength){ + // return; + // } + // } + + let currentScene = controller.connections.obs.local.currentScene || ""; + currentScene = helper.cleanName(currentScene); + + let parameters = { controller, userCommand, accessProfile, channel, message, currentScene } + //check if scene command + try { + let cloudSceneCommand = await checkLocalSceneCommand(...Object.values(parameters)); + let serverSceneCommand = await checkServerSceneCommand(...Object.values(parameters)); + if (cloudSceneCommand || serverSceneCommand) { + //finished processing scene command + return; + } + + //Check time restricted access + let hasAccess = checkTimeAccess(...Object.values(parameters)); + if (!hasAccess) { + //time restricted command + controller.connections.twitch.send(channel, `Time restricted command`); + return + } + + + + //logger.log("current scene",currentScene); + + /* + let customSceneCommand = await checkCustomSceneCommand(controller, userCommand, accessProfile, channel, message, currentScene); + let ptzCommand = await checkPTZCommand(controller, userCommand, accessProfile, channel, message, currentScene); + let nuthouseCommand = await checkNuthouseCommand(controller, userCommand, accessProfile, channel, message, currentScene); + let customcamCommand = await checkCustomCamCommand(controller, userCommand, accessProfile, channel, message, currentScene); + let extraCommand = await checkExtraCommand(controller, userCommand, accessProfile, channel, message, currentScene); + let unifiCommand = await checkUnifiCommand(controller, userCommand, accessProfile, channel, message, currentScene); + */ + let valid = await checkCustomSceneCommand(...Object.values(parameters)) || + await checkPTZCommand(...Object.values(parameters)) || + await checkNuthouseCommand(...Object.values(parameters)) || + await checkCustomCamCommand(...Object.values(parameters)) || + await checkExtraCommand(...Object.values(parameters)) || + await checkUnifiCommand(...Object.values(parameters)) + return valid; + } catch (error) { + logger.log(`Error checking command: ${userCommand}`, error); + } +} + +//Check if Time Restricted Command +function checkTimeAccess(controller, userCommand, accessProfile, channel, message, currentScene) { + let hasAccess = false; + + if (controller.connections.database.timeRestrictionDisabled == true) { + return true; + } + message = message || ""; + let messageArgs = message.split(" "); + let arg1 = messageArgs[1] ?? ""; + arg1 = arg1.trim().toLowerCase(); + if (arg1 == "music"){ + return true; + } + //specific mod time restrictions + if (config.timeRestrictedCommands.includes(userCommand)) { + //check if Admin + if (config.userPermissions.commandPriority[0] == accessProfile.accessLevel) { + hasAccess = true; + //check if super user + } else if (config.userPermissions.commandPriority[1] == accessProfile.accessLevel) { + hasAccess = true; + } + //not directly allowed + if (!hasAccess) { + //check time + let now = new Date(); + // var minutes = now.getMinutes(); + var hour = now.getHours(); + // console.log("check time",now,hour,config.restrictedHours); + if (hour >= config.restrictedHours.start && hour < config.restrictedHours.end) { + //restricted time + hasAccess = false; + } else { + //allow access to mods + hasAccess = true; + } + } + } else { + hasAccess = true; + } + return hasAccess; +} + +async function checkLocalSceneCommand(controller, userCommand, accessProfile, channel, message, currentScene) { + let sceneCommand = false; + + if ( + config.commandScenes[userCommand] != null && + config.commandScenes[userCommand] !== "" + ) { + let hasAccess = false; + //specific mod time restrictions + hasAccess = checkTimeAccess(controller, userCommand, accessProfile); + + //MultiCommand swapping + if (!hasAccess) { + let currentScene = await controller.connections.obs.local.getScene() || ""; + currentScene = helper.cleanName(currentScene); + + for (const baseCommand in config.multiCommands) { + let fullList = config.multiCommands[baseCommand] || []; + //find usercommand in MultiCommands + if (fullList.includes(userCommand)) { + //get Scene names for matching Command + let fullSceneList = config.multiScenes[baseCommand] || []; + for (let i = 0; i < fullSceneList.length; i++) { + let scene = fullSceneList[i] || ""; + scene = helper.cleanName(scene); + //check if current scene is in current commands Multiscenes + if (scene != "" && currentScene == scene) { + hasAccess = true; + break; + } + } + } + } + + //One Direction Swapping (if on scene, allow subscene) + let onWayList = config.onewayCommands[currentScene] || null; + if (onWayList != null) { + if (onWayList.includes(userCommand)) { + hasAccess = true; + } + } + } + + if (hasAccess) { + await controller.connections.obs.local.setScene(config.commandScenes[userCommand]); + sceneCommand = true; + + // if (userCommand == "hankcam2" && !controller.connections.database["hankIR"].status) { + // //enable ir + // controller.connections.cameras["hankcorner"].enableIR(); + // controller.connections.database["hankIR"] = { status: true, time: now() }; + // } else if (userCommand != "hankcam2" && controller.connections.database["hankIR"].status) { + // controller.connections.cameras["hankcorner"].disableIR(); + // controller.connections.database["hankIR"] = {status:false,time:null}; + // } + + //Clear CustomCam list if changed + // if (userCommand != "customcams") { + // clearCustomCamsDB(controller); + // } + } + } + return sceneCommand; +} + +async function checkServerSceneCommand(controller, userCommand, accessProfile, channel, message, currentScene) { + let sceneCommand = false; + + if (config.pauseCloudSceneChange) { + return false; + } + + //check if cloud scene command + if ( + config.commandScenesCloud[userCommand] != null && + config.commandScenesCloud[userCommand] !== "" + ) { + let hasAccess = checkTimeAccess(controller, userCommand, accessProfile); + if (hasAccess) { + setTimeout(() => { + controller.connections.obs.cloud.setScene(config.commandScenesCloud[userCommand]); + }, 500) + sceneCommand = true; + } + } + return sceneCommand; +} + +async function checkCustomSceneCommand(controller, userCommand, accessProfile, channel, message, currentScene) { + + userCommand = userCommand || ""; + userCommand = userCommand.toLowerCase(); + let fullArgs = userCommand; + if (!config.customSceneCommands.includes(userCommand)) { + return; + } + + userCommand = helper.cleanName(userCommand); + let overrideArgs = config.customCommandAlias[userCommand]; + if (overrideArgs != null) { + fullArgs = overrideArgs; + } + switchToCustomCams(controller, channel, accessProfile, "customcamsbig", fullArgs) + return true; +} + +async function checkPTZCommand(controller, userCommand, accessProfile, channel, message, currentScene) { + //check if PTZ command + if (!userCommand.startsWith(config.ptzPrefix)) { + return false; + } + + let messageArgs = message.split(" "); + + let arg1 = messageArgs[1] ?? ""; + let arg2 = messageArgs[2] ?? ""; + let arg3 = messageArgs[3] ?? ""; + let arg4 = messageArgs[4] ?? ""; + let arg5 = messageArgs[5] ?? ""; + + let specificCamera = ""; + let ptzcamName = helper.cleanName(arg1); + //convert to clean base command + let baseName = config.customCommandAlias[ptzcamName] ?? ptzcamName; + //convert to axis camera name + ptzcamName = config.axisCameraCommandMapping[baseName] ?? baseName; + + //console.log("ptzcommand",userCommand,currentScene,"base",baseName,"cam",ptzcamName); + + + if (controller.connections.cameras[ptzcamName] != null) { + specificCamera = ptzcamName; + currentScene = ptzcamName; + arg1 = messageArgs[2] ?? ""; + arg2 = messageArgs[3] ?? ""; + arg3 = messageArgs[4] ?? ""; + arg4 = messageArgs[5] ?? ""; + arg5 = messageArgs[6] ?? ""; + //remove camera argument + messageArgs.splice(0, 1); + } else { + //didnt add a specific modifier + if (currentScene == "custom") { + let currentCamList = controller.connections.database["customcam"]; + let firstScene = currentCamList[0] ?? ""; + //remove fullcam + let cleanFirstScene = helper.cleanName(firstScene) ?? ""; + ptzcamName = config.axisCameraCommandMapping[cleanFirstScene] ?? cleanFirstScene; + if (controller.connections.cameras[ptzcamName] != null) { + specificCamera = ptzcamName; + currentScene = ptzcamName; + } + } + } + + // logger.log("ptzcommand converted","currentscene",currentScene,"specificCamera",specificCamera); + arg1 = arg1.trim().toLowerCase(); + arg2 = arg2.trim().toLowerCase(); + arg3 = arg3.trim().toLowerCase(); + arg4 = arg4.trim().toLowerCase(); + arg5 = arg5.trim().toLowerCase(); + + + let camera = controller.connections.cameras[currentScene] || null; + if (camera == null) { + //multiscene/extra scene + let parentScene = ""; + for (let multiScene in config.multiScenes) { + let sceneList = config.multiScenes[multiScene]; + //check if this is a multiscene and find parent camera + for (let i = 0; i < sceneList.length; i++) { + let sceneName = sceneList[i] || ""; + sceneName = helper.cleanName(sceneName); + if (currentScene == sceneName) { + //found match + parentScene = multiScene; + break; + } + } + if (parentScene != "") { + break; + } + } + //set currentscene and camera to parent + if (parentScene != "") { + currentScene = helper.cleanName(parentScene); + camera = controller.connections.cameras[currentScene] || null; + } + } + + //cant find camera client + if (camera == null) { + return false; + } + + //disable specific camera movement + // if (ptzcamName == "pasture"){ + // return false; + // } + + controller.connections.database[currentScene] = controller.connections.database[currentScene] || {}; + controller.connections.database[currentScene].presets = controller.connections.database[currentScene].presets || {}; + + //roamstatus + //roamlist + //speed + + //Doing Any PTZ Command disables roaming + if (userCommand != "ptzroaminfo") { + controller.connections.database[currentScene].isRoaming = false; + } + + switch (userCommand) { + case "ptzpan": + // logger.log('ptzpan',arg1); + camera.panCamera(arg1); + camera.enableAutoFocus(); + break; + case "ptztilt": + camera.tiltCamera(arg1); + camera.enableAutoFocus(); + break; + case "ptzzoom": + let zscaledAmount = arg1 * 100 || 0; + camera.zoomCamera(zscaledAmount); + camera.enableAutoFocus(); + break; + case "ptzfocus": + if (arg1 == "on" || arg1 == "yes") { + camera.enableAutoFocus(); + } else if (arg1 == "off" || arg1 == "no") { + camera.disableAutoFocus(); + } else { + let fscaledAmount = arg1 * 50 || 0; + camera.focusCameraExact(fscaledAmount); + } + break; + case "ptzfocusr": + if (arg1 == "on" || arg1 == "yes") { + camera.enableAutoFocus(); + } else if (arg1 == "off" || arg1 == "no") { + camera.disableAutoFocus(); + } else { + let fscaledAmount = arg1 * 50 || 0; + camera.focusCamera(fscaledAmount); + } + break; + case "ptzautofocus": + if (arg1 == "1" || arg1 == "on" || arg1 == "yes") { + camera.enableAutoFocus(); + } else if (arg1 == "off" || arg1 == "0" || arg1 == "no") { + camera.disableAutoFocus(); + } + break; + case "ptztracking": + if (arg1 == "1" || arg1 == "on" || arg1 == "yes") { + camera.enableAutoTracking(); + camera.enableAutoFocus(); + } else if (arg1 == "off" || arg1 == "0" || arg1 == "no") { + camera.disableAutoTracking(); + camera.enableAutoFocus(); + } + break; + case "ptzpreset": + if (specificCamera != "") { + //used camera name + if (arg1 != "") { + //2nd argument provided + camera.goToPreset(arg1); + } else { + camera.goToPreset(specificCamera); + } + } else { + camera.goToPreset(arg1); + } + camera.enableAutoFocus(); + break; + case "ptzmove": + camera.moveCamera(arg1); + camera.enableAutoFocus(); + break; + case "ptzspeed": + camera.setSpeed(arg1); + controller.connections.database[currentScene].speed = arg1; + break; + case "ptzset": + //pan tilt zoom relative pos + camera.ptz({ rpan: arg1, rtilt: arg2, rzoom: arg3 * 100, autofocus: "on" }); + break; + case "ptzseta": + //absolute pos, pan tilt zoom autofocus focus + let customPtz = { pan: arg1}; + + let customTilt = parseInt(arg2); + if (!isNaN(customTilt)) { + customPtz.tilt = customTilt; + } + + let customZoom = parseInt(arg3); + if (!isNaN(customZoom)) { + customPtz.zoom = customZoom; + } + + let customAF = arg4; + if (arg4 == "1" || arg4 == "on" || arg4 == "yes") { + customAF = "on"; + } else if (arg4 == "off" || arg4 == "0" || arg4 == "no") { + customAF = "off"; + } else { + customAF = "on"; + } + customPtz.autofocus = customAF; + + let customFocus = parseInt(arg5); + if (!isNaN(customFocus)) { + customPtz.focus = customFocus; + } + camera.ptz(customPtz); + break; + case "ptzgetinfo": + let cpos = await camera.getPosition(); + if (cpos && cpos.pan != null) { + controller.connections.twitch.send(channel, `PTZ Info (${currentScene}): ${cpos.pan}p |${cpos.tilt}t |${cpos.zoom}z |af ${cpos.autofocus || "n/a"} |${cpos.focus || "n/a"}f`); + } else { + logger.log("Failed to get ptz position"); + } + break; + case "ptzspin": + camera.continousPanTilt(arg1, arg2); + break; + case "ptzstop": + camera.continousPanTilt(0, 0); + break; + case "ptzdry": + camera.speedDry(); + break; + case "ptzir": + if (arg1 == "1" || arg1 == "on" || arg1 == "yes") { + camera.setIRCutFilter("off"); + } else if (arg1 == "off" || arg1 == "0" || arg1 == "no") { + camera.setIRCutFilter("on"); + } else { + camera.setIRCutFilter("auto"); + } + break; + case "ptzirlight": + if (arg1 == "1" || arg1 == "on" || arg1 == "yes") { + camera.enableIR(); + } else if (arg1 == "off" || arg1 == "0" || arg1 == "no") { + camera.disableIR(); + } else { + camera.disableIR(); + } + break; + case "ptzsave": + let currentPosition = await camera.getPosition(); + if (currentPosition && currentPosition.pan != null) { + if (specificCamera != "") { + //used camera name + if (arg1 != "") { + //2nd argument provided + controller.connections.database[specificCamera].presets[arg1] = currentPosition; + } else { + controller.connections.database[currentScene].presets[specificCamera] = currentPosition; + } + } else { + if (arg1 != "") { + //save named preset + controller.connections.database[currentScene].presets[arg1] = currentPosition; + } else { + controller.connections.database[currentScene].lastKnownPosition = currentPosition; + } + } + + } else { + logger.log("Failed to get ptz position"); + } + break; + case "ptzhomeold": + camera.goHome(); + camera.enableAutoFocus(); + break; + case "ptzhome": + case "ptzload": + if (userCommand == "ptzhome"){ + arg1 = "home"; + } + if (specificCamera != "") { + //used camera name + if (arg1 != "") { + //2nd argument provided + let preset = controller.connections.database[specificCamera].presets[arg1]; + if (preset != null) { + camera.ptz({ pan: preset.pan, tilt: preset.tilt, zoom: preset.zoom, focus: preset.focus, autofocus: preset.autofocus }); + } else if (controller.connections.database[specificCamera].lastKnownPosition != null) { + let previous = controller.connections.database[specificCamera].lastKnownPosition; + camera.ptz({ pan: previous.pan, tilt: previous.tilt, zoom: previous.zoom }); + } + + } else { + let preset = controller.connections.database[currentScene].presets[specificCamera]; + if (preset != null) { + camera.ptz({ pan: preset.pan, tilt: preset.tilt, zoom: preset.zoom, focus: preset.focus, autofocus: preset.autofocus }); + } else if (controller.connections.database[currentScene].lastKnownPosition != null) { + let previous = controller.connections.database[currentScene].lastKnownPosition; + camera.ptz({ pan: previous.pan, tilt: previous.tilt, zoom: previous.zoom }); + } + } + } else { + let preset = controller.connections.database[currentScene].presets[arg1]; + if (preset != null) { + camera.ptz({ pan: preset.pan, tilt: preset.tilt, zoom: preset.zoom, focus: preset.focus, autofocus: preset.autofocus }); + } else if (controller.connections.database[currentScene].lastKnownPosition != null) { + let previous = controller.connections.database[currentScene].lastKnownPosition; + camera.ptz({ pan: previous.pan, tilt: previous.tilt, zoom: previous.zoom }); + } + } + break; + case "ptzremove": + if (specificCamera != "") { + //used camera name + if (arg1 != "") { + //2nd argument provided + if (controller.connections.database[specificCamera].presets[arg1] != null) { + let response = delete controller.connections.database[specificCamera].presets[arg1]; + if (response != true) { + logger.log(`Failed to remove preset ${arg1}: ${response} ${controller.connections.database[specificCamera]}`); + } + } + } else { + controller.connections.database[currentScene].presets[specificCamera] = currentPosition; + } + } else { + if (controller.connections.database[currentScene].presets[arg1] != null) { + let response = delete controller.connections.database[currentScene].presets[arg1]; + if (response != true) { + logger.log(`Failed to remove preset ${arg1}: ${response} ${controller.connections.database[currentScene]}`); + } + } + } + break; + case "ptzrename": + if (specificCamera != "") { + //used camera name + if (arg1 != "" && arg2 != "") { + //2nd argument provided + if (controller.connections.database[specificCamera].presets[arg1] != null) { + controller.connections.database[specificCamera].presets[arg2] = controller.connections.database[specificCamera].presets[arg1]; + let response = delete controller.connections.database[specificCamera].presets[arg1]; + if (response != true) { + logger.log(`Failed to remove preset ${arg1}: ${response} ${controller.connections.database[specificCamera]}`); + } + } + } else { + if (controller.connections.database[currentScene].presets[specificCamera] != null) { + controller.connections.database[currentScene].presets[arg1] = controller.connections.database[currentScene].presets[specificCamera]; + let response = delete controller.connections.database[currentScene].presets[specificCamera]; + if (response != true) { + logger.log(`Failed to remove preset ${specificCamera}: ${response} ${controller.connections.database[currentScene]}`); + } + } + } + } else { + if (controller.connections.database[currentScene].presets[arg1] != null) { + controller.connections.database[currentScene].presets[arg2] = controller.connections.database[currentScene].presets[arg1]; + let response = delete controller.connections.database[currentScene].presets[arg1]; + if (response != true) { + logger.log(`Failed to remove preset ${arg1}: ${response} ${controller.connections.database[currentScene]}`); + } + } + } + break; + case "ptzclear": + if (specificCamera) { + controller.connections.database[specificCamera].presets = {}; + } else { + controller.connections.database[currentScene].presets = {}; + } + break; + case "ptzlist": + if (specificCamera) { + controller.connections.twitch.send(channel, `PTZ Presets: ${Object.keys(controller.connections.database[specificCamera].presets).sort().toString()}`) + } else { + controller.connections.twitch.send(channel, `PTZ Presets: ${Object.keys(controller.connections.database[currentScene].presets).sort().toString()}`) + } + + break; + case "ptzgetfocus": + let pos = await camera.getPosition(); + let currentFocus = parseFloat(pos.focus); + if (!isNaN(currentFocus)) { + //is number + try { + currentFocus = currentFocus / 50; + currentFocus.toFixed(2); + controller.connections.twitch.send(channel, `PTZ Focus (0-200): ${currentFocus}`) + } catch (e) { + //logger.log("Error getting focus") + } + + } + break; + case "ptzroam": + let currentSpeed = await camera.getSpeed(); + if (messageArgs.length == 2 && (arg1 == "on" || arg1 == "1" || arg1 == "yes")) { + //start roaming + controller.connections.database[currentScene].isRoaming = true; + if (currentSpeed != null) { + controller.connections.database[currentScene].speed = currentSpeed; + } + clearTimeout(roamTimeout); + setPTZRoamMode(controller, currentScene); + break; + } else if (messageArgs.length == 2 && (arg1 == "off" || arg1 == "0" || arg1 == "no")) { + //stop roaming + controller.connections.database[currentScene].isRoaming = false; + break; + } else if (messageArgs.length < 4) { + //invalid command + break; + } + //ptzroam on/off + //ptzroam 10s 20 fence barn trailer + //ptzroam 10 fence barn trailer + //ptzroam fence barn trailer + + let startingListPos = 0; + if (!isNaN(parseInt(arg1))) { + //length of time + controller.connections.database[currentScene].roamTime = arg1; + startingListPos = 2; + } else { + //invalid command + break; + } + if (!isNaN(parseFloat(arg2))) { + //speed given + controller.connections.database[currentScene].roamSpeed = arg2; + startingListPos = 3; + } + let ptzRoamList = []; + for (let i = startingListPos; i < messageArgs.length; i++) { + let position = messageArgs[i]; + if (controller.connections.database[currentScene].presets[position] != null) { + ptzRoamList.push(position); + } + } + controller.connections.database[currentScene].roamIndex = -1; + controller.connections.database[currentScene].roamDirection = "forward"; + controller.connections.database[currentScene].roamList = JSON.parse(JSON.stringify(ptzRoamList)); + controller.connections.database[currentScene].isRoaming = true; + + if (currentSpeed != null) { + controller.connections.database[currentScene].speed = currentSpeed; + } + + clearTimeout(roamTimeout); + setPTZRoamMode(controller, currentScene); + break; + case "ptzroaminfo": + try { + let isEnabled = "Disabled"; + if (controller.connections.database[currentScene].isRoaming) { + isEnabled = "Enabled"; + } + controller.connections.twitch.send(channel, `PTZ Roam: ${isEnabled} ${controller.connections.database[currentScene].roamTime} ${controller.connections.database[currentScene].roamSpeed} ${controller.connections.database[currentScene].roamList}`) + } catch (e) { + logger.log("Error sending ptzroaminfo") + } + break; + default: + logger.log(`Invalid PTZ Command: ${userCommand}`); + return false; + } + return true; +} + +async function checkNuthouseCommand(controller, userCommand, accessProfile, channel, message, currentScene) { + if (userCommand == null || currentScene != "nuthouse") { + return false; + } + let messageArgs = message.split(" "); + + let arg1 = messageArgs[1] || ""; + let arg2 = messageArgs[2] || ""; + let arg3 = messageArgs[3] || ""; + + arg1 = arg1.trim().toLowerCase(); + arg2 = arg2.trim().toLowerCase(); + arg3 = arg3.trim().toLowerCase(); + + switch (userCommand) { + case "ptzpan": + // logger.log('ptzpan',arg1); + controller.connections.obsBot.pan(arg1); + break; + case "ptztilt": + controller.connections.obsBot.tilt(arg1); + break; + case "ptzzoom": + controller.connections.obsBot.setZoom(arg1); + break; + case "ptzfov": + controller.connections.obsBot.setFOV(arg1); + break; + case "ptztracking": + controller.connections.obsBot.setTracking(arg1); + break; + case "ptzhome": + controller.connections.obsBot.resetPosition(); + break; + case "ptzpreset": + let num = 1; + if (arg1 == "counter") { + num = 1; + } else if (arg1 == "table" || arg1 == "bench") { + num = 2; + } else if (arg1 == "room") { + num = 3; + } + controller.connections.obsBot.setPreset(num); + break; + case "ptzstop": + controller.connections.obsBot.stop(); + break; + case "ptzwake": + controller.connections.obsBot.wake(); + break; + default: + logger.log(`Invalid OBSBot Command: ${userCommand}`); + return false; + } + //finish OBSBot commands + return true; +} + +async function checkCustomCamCommand(controller, userCommand, accessProfile, channel, message, currentScene) { + if (userCommand == null || currentScene != "custom") { + return false; + } + let messageArgs = message.split(" "); + + let arg1 = messageArgs[1] ?? ""; + let arg2 = messageArgs[2] ?? ""; + let arg3 = messageArgs[3] ?? ""; + + arg1 = arg1.trim().toLowerCase(); + arg2 = arg2.trim().toLowerCase(); + arg3 = arg3.trim().toLowerCase(); + + let fullArgs = message.split(' ').slice(1).join(' ') + let currentCamList = controller.connections.database["customcam"] ?? []; + let currentUserCommand = controller.connections.database["customcamscommand"] ?? "customcams"; + + controller.connections.database["layoutpresets"] = controller.connections.database["layoutpresets"] || {}; + + // logger.log("Cam Save",userCommand,currentCamList,currentUserCommand); + let preset = null; + switch (userCommand) { + case "camsave": + if (currentCamList.length > 0) { + if (arg1 != "") { + //2nd argument provided + controller.connections.database["layoutpresets"][arg1] = { list: currentCamList, command: currentUserCommand }; + } else { + controller.connections.database["layoutpresets"]["temporarylayoutsave"] = { list: currentCamList, command: currentUserCommand }; + } + } else { + logger.log("Failed to save layout. No current cam list"); + } + break; + case "camload": + //2nd argument provided + preset = controller.connections.database["layoutpresets"]?.[arg1] ?? null; + if (preset != null && preset.list && preset.command) { + let chatmessage = preset.list.join(' '); + switchToCustomCams(controller, channel, accessProfile, preset.command, chatmessage) + } else if (arg1 == "") { + let previous = controller.connections.database["layoutpresets"]["temporarylayoutsave"] ?? {}; + if (previous != null && previous.list && previous.command) { + let chatmessage = previous.list.join(' '); + switchToCustomCams(controller, channel, accessProfile, previous.command, chatmessage) + } + } + break; + case "campresetremove": + preset = controller.connections.database["layoutpresets"]?.[arg1] ?? null; + if (preset != null) { + let response = delete controller.connections.database["layoutpresets"][arg1]; + if (response != true) { + logger.log(`Failed to remove cam preset ${arg1}: ${response} ${controller.connections.database["layoutpresets"]}`); + } + } + break; + case "camrename": + preset = controller.connections.database["layoutpresets"]?.[arg1] ?? null; + if (preset != null) { + controller.connections.database["layoutpresets"][arg2] = preset; + let response = delete controller.connections.database["layoutpresets"][arg1]; + if (response != true) { + logger.log(`Failed to rename cam preset ${arg1}, ${arg2}: ${response} ${controller.connections.database["layoutpresets"]}`); + } + } + break; + case "camclear": + controller.connections.database["layoutpresets"] = {}; + break; + case "camlist": + controller.connections.twitch.send(channel, `Cam Layout Presets: ${Object.keys(controller.connections.database["layoutpresets"]).toString()}`) + break; + default: + // logger.log(`Invalid Cam Command: ${userCommand}`); + return false; + } + //finish Cam commands + return true; +} + +async function checkExtraCommand(controller, userCommand, accessProfile, channel, message, currentScene) { + //extra + message = message.trim(); + let messageArgs = message.split(" "); + let arg1 = messageArgs[1] || ""; + let arg2 = messageArgs[2] || ""; + let arg3 = messageArgs[3] || ""; + let arg4 = messageArgs[4] || ""; + arg1 = arg1.trim(); + arg2 = arg2.trim(); + let arg1Clean = helper.cleanName(arg1) ?? ""; + let arg2Clean = helper.cleanName(arg2) ?? ""; + + let arg1Base = config.customCommandAlias[arg1Clean] ?? arg1Clean; + + let fullArgs = message.split(' ').slice(1).join(' '); + let argsList = fullArgs.split(" "); + let audioSource = ""; + let currentCamList = controller.connections.database["customcam"]; + let currentCustomScene = currentCamList[0] ?? ""; + currentCustomScene = helper.cleanName(currentCustomScene) ?? ""; + if (currentCustomScene == "") { + currentCustomScene = currentScene + } + let currentSceneBase = config.customCommandAlias[currentCustomScene] ?? currentCustomScene; + + + // logger.log("Extra Command",accessProfile,userCommand,fullArgs,currentScene,currentCamList); + + switch (userCommand) { + case "resetsourcef": + controller.connections.obs.local.restartSource(fullArgs); + break; + case "resetcloudsourcef": + controller.connections.obs.cloud.restartSource(fullArgs); + break; + case "resetbackpackf": + controller.connections.obs.cloud.restartSource("Maya RTMP 1"); + controller.connections.obs.cloud.restartSource("RTMP Mobile"); + controller.connections.obs.cloud.restartSource("Space RTMP Backpack"); + break; + case "resetlivecamf": + controller.connections.obs.cloud.restartSource("Maya RTMP 2"); + controller.connections.obs.cloud.restartSource("RTMP AlveusStudio"); + controller.connections.obs.cloud.restartSource("Space RTMP Server"); + break; + case "resetpcf": + controller.connections.obs.cloud.restartSource("Maya RTMP 3"); + controller.connections.obs.cloud.restartSource("RTMP AlveusDesktop"); + controller.connections.obs.cloud.restartSource("Space RTMP Desktop"); + break; + case "resetphonef": + controller.connections.obs.cloud.restartSource("RTMP Mobile"); + break; + case "resetsource": + controller.connections.obs.local.restartSceneItem(controller.connections.obs.local.currentScene, fullArgs); + break; + case "resetcloudsource": + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, fullArgs); + break; + case "resetbackpack": + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "Maya RTMP 1"); + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "RTMP Mobile"); + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "Space RTMP Backpack"); + break; + case "resetlivecam": + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "Maya RTMP 2"); + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "RTMP AlveusStudio"); + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "Space RTMP Server"); + break; + case "resetpc": + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "Maya RTMP 3"); + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "RTMP AlveusDesktop"); + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "Space RTMP Desktop"); + break + case "resetphone": + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "RTMP Mobile"); + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "Space RTMP Phone"); + break + case "resetextra": + controller.connections.obs.cloud.restartSceneItem(controller.connections.obs.cloud.currentScene, "Space RTMP Extra"); + break + case "resetcam": + let camname = arg1Clean; + //remove possible cam wording + camname = "fullcam " + camname; + controller.connections.obs.local.restartSceneItem(controller.connections.obs.local.currentScene, camname); + break; + case "setalveusscene": + controller.connections.obs.local.setScene(fullArgs); + clearCustomCamsDB(controller); + break; + case "setcloudscene": + controller.connections.obs.cloud.setScene(fullArgs); + clearCustomCamsDB(controller); + break; + case "setrestricted": + //toggle time restrictions + if (arg1 == "1" || arg1 == "on" || arg1 == "yes") { + controller.connections.database.timeRestrictionDisabled = false; + } else if (arg1 == "off" || arg1 == "0" || arg1 == "no") { + controller.connections.database.timeRestrictionDisabled = true; + } else { + controller.connections.database.timeRestrictionDisabled = false; + } + break; + case "changeserver": + logger.log("changing server to:", arg1); + let invalid = false; + if (arg1 == "maya") { + controller.connections.obs.cloud.disconnect(); + controller.connections.obs.cloud = await controller.connections.obs.create("cloudMaya"); + } else if (arg1 == "alveus") { + controller.connections.obs.cloud.disconnect(); + controller.connections.obs.cloud = await controller.connections.obs.create("cloudAlveus"); + } else if (arg1 == "space") { + controller.connections.obs.cloud.disconnect(); + controller.connections.obs.cloud = await controller.connections.obs.create("cloudSpace"); + } else { + //invalid + invalid = true; + } + setTimeout(async function () { + let cloudLive = await controller.connections.obs.cloud.isLive() || false; + let serverName = controller.connections.obs.cloud.name; + logger.log("Change Server Status: ", serverName, cloudLive, controller.connections.obs.cloud.name); + if (cloudLive) { + controller.connections.twitch.send(channel, `Cloud Server changed to ${serverName}`); + } else { + controller.connections.twitch.send(channel, `Cloud Server-${serverName} offline`); + } + }, 5000); + if (!invalid) { + controller.connections.database.cloudServer = arg1; + } + break; + case "setmute": + let muteStatus = null; + if (arg2 == "1" || arg2 == "on" || arg2 == "yes") { + muteStatus = true; + } else if (arg2 == "off" || arg2 == "0" || arg2 == "no") { + muteStatus = false; + } else { + muteStatus = arg2; + } + if (arg1 == "" || arg1 == "mic") { + audioSource = config.sceneAudioSource[currentSceneBase]; + } else { + audioSource = config.sceneAudioSource[arg1Base]; + } + if (audioSource == null || audioSource == "") { + audioSource = arg1; + } + controller.connections.obs.local.setMute(audioSource, muteStatus); + break; + case "mutecam": + if (arg1 == "" || arg1 == "mic") { + audioSource = config.sceneAudioSource[currentSceneBase]; + controller.connections.obs.local.setMute(audioSource, true); + } else if (arg1 == "all") { + for (const source in config.micGroups["livecams"]) { + controller.connections.obs.local.setMute(config.micGroups["livecams"][source].name, true); + } + for (const source in config.micGroups["restrictedcams"]) { + controller.connections.obs.local.setMute(config.micGroups["restrictedcams"][source].name, true); + } + controller.connections.obs.cloud.setMute(config.globalMusicSource, false); + } else if (arg1 == "music") { + controller.connections.obs.cloud.setMute(config.globalMusicSource, true); + } else { + audioSource = config.sceneAudioSource[arg1Base]; + + if (audioSource == null || audioSource == "") { + audioSource = arg1; + } + + controller.connections.obs.local.setMute(audioSource, true); + } + break; + case "unmutecam": + if (arg1 == "all") { + for (const source in config.micGroups["livecams"]) { + controller.connections.obs.local.setInputVolume(config.micGroups["livecams"][source].name, config.micGroups["livecams"][source].volume); + controller.connections.obs.local.setMute(config.micGroups["livecams"][source].name, false); + } + controller.connections.obs.cloud.setMute(config.globalMusicSource, true); + } else if (arg1 == "music") { + controller.connections.obs.cloud.setMute(config.globalMusicSource, false); + } else { + if (arg1 == "" || arg1 == "mic") { + audioSource = config.sceneAudioSource[currentSceneBase]; + } else { + audioSource = config.sceneAudioSource[arg1Base]; + if (audioSource == null || audioSource == "") { + audioSource = arg1; + } + } + let hasAccess = false; + if (Object.keys(config.micGroups["livecams"]).includes(audioSource)) { + hasAccess = true; + } else { + //check if Admin + if (config.userPermissions.commandPriority[0] == accessProfile.accessLevel) { + hasAccess = true; + //check if super user + } else if (config.userPermissions.commandPriority[1] == accessProfile.accessLevel) { + hasAccess = true; + } + } + if (hasAccess) { + controller.connections.obs.local.setMute(audioSource, false); + } + + } + break; + case "muteallcams": + for (const source in config.micGroups["livecams"]) { + controller.connections.obs.local.setMute(config.micGroups["livecams"][source].name, true); + } + for (const source in config.micGroups["restrictedcams"]) { + controller.connections.obs.local.setMute(config.micGroups["restrictedcams"][source].name, true); + } + controller.connections.obs.cloud.setMute(config.globalMusicSource, false); + break; + case "unmuteallcams": + for (const source in config.micGroups["livecams"]) { + controller.connections.obs.local.setInputVolume(config.micGroups["livecams"][source].name, config.micGroups["livecams"][source].volume); + controller.connections.obs.local.setMute(config.micGroups["livecams"][source].name, false); + } + controller.connections.obs.cloud.setMute(config.globalMusicSource, true); + break; + case "removecam": + if (currentScene != "custom") { + return false; + } + + let newListRemoveCam = currentCamList.slice(); + + + for (let arg of argsList) { + if (arg != null && arg != "") { + // logger.log("arg",argsList,arg); + let camName = helper.cleanName(arg); + + let overrideArgs = config.customCommandAlias[camName]; + if (overrideArgs != null) { + //allow alias to change entire argument + let newArgs = overrideArgs.split(" "); + if (newArgs.length > 1) { + for (let newarg of newArgs) { + if (newarg != "") { + argsList.push(newarg); + } + } + continue; + } + camName = overrideArgs; + } + + //camName = "fullcam"+camName; + + //remove cam + for (let i = 0; i < newListRemoveCam.length; i++) { + if (newListRemoveCam[i].includes(camName)) { + newListRemoveCam.splice(i, 1); + } + } + // let index = newListRemoveCam.indexOf(camName); + // if (index !== -1) { + // newListRemoveCam.splice(index, 1); + // } + } + } + + if (newListRemoveCam.length > 0) { + fullArgs = newListRemoveCam.join(' '); + + logger.log(`Remove Cams: ${argsList} - new fullargs: ${fullArgs}`); + switchToCustomCams(controller, channel, accessProfile, userCommand, fullArgs); + } + break; + case "addcam": + // logger.log("addcam",currentCamList,argsList,currentScene) + if (currentScene != "custom") { + return false; + } + + let newListAddCam = currentCamList.slice(); + + for (let arg of argsList) { + if (arg != null && arg != "") { + // logger.log("arg",argsList,arg); + let camName = helper.cleanName(arg); + + let overrideArgs = config.customCommandAlias[camName]; + logger.log("addcam alias", config.customCommandAlias, camName, overrideArgs) + if (overrideArgs != null) { + //allow alias to change entire argument + let newArgs = overrideArgs.split(" "); + if (newArgs.length > 1) { + for (let newarg of newArgs) { + if (newarg != "") { + newListAddCam.push(newarg); + } + } + continue; + } + camName = overrideArgs; + } + camName = "fullcam" + camName; + //add cam + if (!newListAddCam.includes(camName)) { + newListAddCam.push(camName); + } + } + } + + if (newListAddCam.length > 0) { + fullArgs = newListAddCam.join(' '); + + logger.log(`Add Cams: ${argsList} - new fullargs: ${fullArgs}`); + switchToCustomCams(controller, channel, accessProfile, userCommand, fullArgs); + } + break; + case "swapcam": + if (currentScene != "custom") { + return false; + } + + userCommand = controller.connections.database["customcamscommand"] ?? "customcams"; + + // if (controller.connections.database["customcamsbig"]) { + // userCommand = "customcamsbig"; + // } else { + // userCommand = "customcams"; + // } + if (arg1 == "" || arg2 == "") { + if (currentCamList.length == 2) { + let temp = currentCamList[0]; + currentCamList[0] = currentCamList[1]; + currentCamList[1] = temp; + fullArgs = currentCamList.join(' '); + switchToCustomCams(controller, channel, accessProfile, userCommand, fullArgs); + return true; + } else { + return false; + } + } + //replace current cam list with new position + // swapcam 4 cam or swapcam cam cam2 + + let cam1 = helper.cleanName(arg1); + cam1 = config.customCommandAlias[cam1] || cam1; + cam1 = "fullcam" + cam1; + let pos1 = currentCamList.indexOf(cam1); + if (pos1 == -1) { + //get cam at location + pos1 = parseInt(arg1); + if (!isNaN(pos1)) { + pos1 = pos1 - 1 + } else { + pos1 = null; + } + + } + + let cam2 = helper.cleanName(arg2); + cam2 = config.customCommandAlias[cam2] || cam2; + cam2 = "fullcam" + cam2; + let pos2 = currentCamList.indexOf(cam2); + if (pos2 == -1) { + //get cam at location + pos2 = parseInt(arg2); + if (!isNaN(pos2)) { + pos2 = pos2 - 1 + } else { + pos2 = null; + } + + } + + //if both are valid cams or positions, swap them + //if not, replace pos1 with cam2 + let newList = currentCamList.slice(); + + if (pos1 != null && pos2 != null) { + let temp1 = newList[pos1]; + newList[pos1] = newList[pos2]; + newList[pos2] = temp1; + } else if (pos1 != null) { + newList[pos1] = cam2; + } else if (pos2 != null) { + newList[pos2] = cam1; + } else { + break; + } + + //fill empty slots with nocam + for (let i = 0; i < newList.length; i++) { + if (newList[i] == null || newList[i] == "") { + newList[i] = "fullcamremove"; + } + } + newList = newList.filter(i => i != "fullcamremove"); + + if (newList.length > 0) { + fullArgs = newList.join(' '); + + logger.log(`Swap Cam ${cam1} to ${cam2} - fullargs: ${fullArgs}`); + switchToCustomCams(controller, channel, accessProfile, userCommand, fullArgs); + } + break; + // case "nightcams": + // fullArgs = "pasture parrot fox crow"; + // switchToCustomCams(controller, channel, accessProfile, "customcams", fullArgs); + // break; + // case "nightcamsbig": + // fullArgs = "marmoset pasture parrot fox foxcorner crowmulti"; + // switchToCustomCams(controller, channel, accessProfile, "customcamsbig", fullArgs); + // break; + // case "indoorcams": + // fullArgs = "georgie noodle isopods roaches"; + // switchToCustomCams(controller, channel, accessProfile, "customcams", fullArgs); + // break; + // case "indoorcamsbig": + // fullArgs = "georgie noodle isopods roaches"; + // switchToCustomCams(controller, channel, accessProfile, "customcamsbig", fullArgs); + // break; + case "customcamstl": + case "customcamstr": + case "customcamsbl": + case "customcamsbr": + case "customcamsbig": + case "customcams": + switchToCustomCams(controller, channel, accessProfile, userCommand, fullArgs) + break; + case "mutemusic": + controller.connections.obs.cloud.setMute(config.globalMusicSource, true); + break; + case "unmutemusic": + controller.connections.obs.cloud.setMute(config.globalMusicSource, false); + break; + case "mutemusiclocal": + controller.connections.obs.local.setMute(config.globalMusicSource, true); + break; + case "unmutemusiclocal": + controller.connections.obs.local.setMute(config.globalMusicSource, false); + break; + case "musicvolume": + let amount = parseInt(arg1) || 0; + let scaledVol = 100 - amount; + scaledVol = 0 - scaledVol; + controller.connections.obs.local.setInputVolume(config.globalMusicSource, scaledVol); + controller.connections.obs.cloud.setInputVolume(config.globalMusicSource, scaledVol); + break; + case "musicnext": + controller.connections.obs.local.nextMediaSource(config.globalMusicSource); + controller.connections.obs.cloud.nextMediaSource(config.globalMusicSource); + break; + case "musicprev": + controller.connections.obs.local.prevMediaSource(config.globalMusicSource); + controller.connections.obs.cloud.prevMediaSource(config.globalMusicSource); + break; + case "setvolume": + audioSource = ""; + let inputVol = arg2; + if (arg1 == "all") { + let amount2 = parseInt(inputVol) || 0; + let scaledVol2 = amount2 - 100; + for (const source in config.micGroups["livecams"]) { + controller.connections.obs.local.setInputVolume(config.micGroups["livecams"][source].name, scaledVol2); + } + } else { + if (arg1 == "" || arg1 == "mic" || arg1 == "mics" || arg1 == "cam" || arg1 == "cams") { + audioSource = config.sceneAudioSource[currentSceneBase]; + inputVol = arg2; + } if (arg1 == "music") { + audioSource = config.globalMusicSource + inputVol = arg2; + } else { + audioSource = config.sceneAudioSource[arg1Clean]; + if (audioSource == null || audioSource == "") { + audioSource = arg1; + } + inputVol = arg2; + } + let amount2 = parseInt(inputVol) || 0; + let scaledVol2 = amount2 - 100; + if (arg1.includes("music")) { + audioSource = config.globalMusicSource; + } + controller.connections.obs.local.setInputVolume(audioSource, scaledVol2); + if (audioSource == config.globalMusicSource){ + controller.connections.obs.cloud.setInputVolume(config.globalMusicSource, scaledVol2); + } + } + break; + case "getvolume": + if (arg1 == "" || arg1 == "all") { + let output = ""; + for (const source in config.micGroups["livecams"]) { + let dbVolume = await controller.connections.obs.local.getInputVolume(config.micGroups["livecams"][source].name); + dbVolume = parseInt(dbVolume); + if (!isNaN(dbVolume)) { + let correctedVol = 100 + dbVolume; + + let isMuted = await controller.connections.obs.local.getMute(config.micGroups["livecams"][source].name); + let muteStatus = ""; + if (isMuted) { + muteStatus = "m"; + } + output = `${output}${source} - ${correctedVol.toFixed(1).replace(/[.,]0$/, "")}${muteStatus}, `; + } + } + for (const source in config.micGroups["restrictedcams"]) { + let dbVolume = await controller.connections.obs.local.getInputVolume(config.micGroups["restrictedcams"][source].name); + dbVolume = parseInt(dbVolume); + if (!isNaN(dbVolume)) { + let correctedVol = 100 + dbVolume; + + let isMuted = await controller.connections.obs.local.getMute(config.micGroups["restrictedcams"][source].name); + let muteStatus = ""; + if (isMuted) { + muteStatus = "m"; + } + output = `${output}${source} - ${correctedVol.toFixed(1).replace(/[.,]0$/, "")}${muteStatus}, `; + } + } + //get music + let dbVolume = await controller.connections.obs.cloud.getInputVolume("Music Playlist Global"); + dbVolume = parseInt(dbVolume); + if (!isNaN(dbVolume)) { + let correctedVol = 100 + dbVolume; + + let isMuted = await controller.connections.obs.cloud.getMute("Music Playlist Global"); + let muteStatus = ""; + if (isMuted) { + muteStatus = "m"; + } + output = `${output}music - ${correctedVol.toFixed(1).replace(/[.,]0$/, "")}${muteStatus}, `; + } + + // logger.log('getvolume all',output); + controller.connections.twitch.send(channel, `Volumes: ${output}`) + } else { + let volName = arg1Clean; + if (arg1 == "mic" || arg1 == "mics" || arg1 == "cam" || arg1 == "cams") { + audioSource = config.sceneAudioSource[currentSceneBase]; + volName = currentSceneBase; + } else { + audioSource = config.sceneAudioSource[arg1Clean]; + if (audioSource == null || audioSource == "") { + audioSource = arg1; + } + } + if (arg1 == "music") { + //get music + let dbVolume = await controller.connections.obs.cloud.getInputVolume("Music Playlist Global"); + dbVolume = parseInt(dbVolume); + if (!isNaN(dbVolume)) { + let correctedVol = 100 + dbVolume; + + let isMuted = await controller.connections.obs.cloud.getMute("Music Playlist Global"); + let muteStatus = ""; + if (isMuted) { + muteStatus = "m"; + } + controller.connections.twitch.send(channel, `Music Volume: ${correctedVol.toFixed(1).replace(/[.,]0$/, "")}${muteStatus}`) + } + } else { + let dbVolume = await controller.connections.obs.local.getInputVolume(audioSource); + dbVolume = parseInt(dbVolume); + if (!isNaN(dbVolume)) { + let correctedVol = 100 + dbVolume; + let isMuted = await controller.connections.obs.local.getMute(audioSource); + logger.log("getvolume", audioSource, isMuted); + let muteStatus = ""; + if (isMuted) { + muteStatus = "m"; + } + // logger.log("Setting",correctedVol); + controller.connections.twitch.send(channel, `Volume: ${volName} - ${correctedVol.toFixed(1).replace(/[.,]0$/, "")}${muteStatus}`) + } + } + } + break; + case "resetvolume": + for (const source in config.micGroups["livecams"]) { + controller.connections.obs.local.setInputVolume(config.micGroups["livecams"][source].name, config.micGroups["livecams"][source].volume); + } + break; + default: + return false; + } + + return true; +} + +async function checkUnifiCommand(controller, userCommand, accessProfile, channel, message, currentScene) { + + let messageArgs = message.split(" "); + let arg1 = messageArgs[1] || ""; + let arg2 = messageArgs[2] || ""; + let arg3 = messageArgs[3] || ""; + let arg4 = messageArgs[4] || ""; + arg1 = arg1.trim(); + arg2 = arg2.trim(); + + let apClient, name, ap_name, signal, chatMessage, response = null; + + switch (userCommand) { + case "apsignal": + apClient = await controller.connections.unifi.getSignal("liveu"); + if (apClient) { + signal = apClient.signal; + ap_name = apClient.ap_name; + if (ap_name.includes(":") || controller.connections.unifi.isValidMacAddress(ap_name)) { + ap_name = "AlveusAP" + } + chatMessage = `LiveU Signal ${signal}(${ap_name})`; + } else { + chatMessage = `LiveU Not Found`; + } + controller.connections.twitch.send(channel, chatMessage); + break; + case "apreconnect": + response = await controller.connections.unifi.clientReconnect("liveu"); + if (response) { + controller.connections.twitch.send(channel, `Reconnecting LiveU`); + } else { + controller.connections.twitch.send(channel, `Reconnecting LiveU Failed`); + } + break; + case "apclientinfo": + apClient = await controller.connections.unifi.getSignal(arg1); + if (apClient) { + signal = apClient.signal; + ap_name = apClient.ap_name; + if (ap_name.includes(":") || controller.connections.unifi.isValidMacAddress(ap_name)) { + ap_name = "AlveusAP" + } + chatMessage = `${arg1} signal ${signal}(${ap_name})`; + } else { + chatMessage = `${arg1} not found`; + } + controller.connections.twitch.send(channel, chatMessage); + break; + case "apclientreconnect": + response = await controller.connections.unifi.clientReconnect(arg1); + if (response) { + controller.connections.twitch.send(channel, `Reconnecting ${arg1}`); + } else { + controller.connections.twitch.send(channel, `Reconnecting ${arg1} Failed`); + } + break; + default: + return false; + } + + return true; +} + +function clearCustomCamsDB(controller) { + controller.connections.database["customcam"] = []; +} + +async function switchToCustomCams(controller, channel, accessProfile, userCommand, fullArgs) { + console.log("switch", channel, accessProfile, userCommand, fullArgs); + let obsSources = await controller.connections.obs.local.getSceneItemList("Custom Cams") || []; //controller.connections.obs.local.sceneList || []; + let obsList = []; + let currentCamList = []; + for (let scene of obsSources) { + let name = scene.sourceName; + let enabled = scene.sceneItemEnabled; + if (name.includes("fullcam")) { + let sceneName = helper.cleanName(name); + obsList.push(sceneName); + if (enabled) { + currentCamList.push(sceneName); + } + } + } + + // let currentCamList = controller.connections.database["customcam"]; + fullArgs = fullArgs ?? ""; + fullArgs = fullArgs.trim(); + let argsList = fullArgs.split(" "); + argsList.splice(6); + let camList = []; + + // logger.log("user access profile", accessProfile); + let validCommand = true; + let invalidAccess = false; + + let newArgList = []; + //convert to basenames + for (let arg of argsList) { + if (arg != null && arg != "") { + // logger.log("arg",argsList,arg); + let camName = helper.cleanName(arg); + + let overrideArgs = config.customCommandAlias[camName]; + // logger.log("ccam alias",config.customCommandAlias,camName,overrideArgs) + if (overrideArgs != null) { + //allow alias to change entire argument + let newArgs = overrideArgs.split(" "); + if (newArgs.length > 1) { + for (let newarg of newArgs) { + if (newarg != "") { + newArgList.push(newarg); + } + } + continue; + } + camName = overrideArgs; + } + + newArgList.push(camName); + } + } + //check access for cams + for (let camName of newArgList) { + + let fullCamName = "fullcam" + camName; + + // logger.log("arg",arg,camName,fullCamName); + //check if valid source + if (!obsList.includes(camName)) { + if (camName != "empty" && camName != "blank") { + validCommand = false; + break; + } + } + + let hasAccess = false; + if (currentCamList.includes(camName) || camName == "empty" || camName == "blank") { + hasAccess = true; + } + + //convert name to base name and check if multiscene + let baseCamName = config.multiCustomCamScenesConverted[camName] || ""; + if (!hasAccess && baseCamName != null && baseCamName != "") { + for (let currcam of currentCamList) { + currcam = currcam ?? ""; + // currcam = helper.cleanName(currcam); + let baseName = config.multiCustomCamScenesConverted[currcam] || ""; + //newcam is part of a current cam multiscene + if (baseCamName != "" && baseCamName == baseName) { + hasAccess = true; + break; + } + } + } + + if (!hasAccess) { + //Admin + if (config.userPermissions.commandPriority[0] == accessProfile.accessLevel) { + hasAccess = true; + //Superuser + } else if (config.userPermissions.commandPriority[1] == accessProfile.accessLevel) { + hasAccess = true; + //Mod + } else if (!config.timeRestrictedScenes.includes(camName)) { + logger.log("Reached Regular Access", "Non time restricted", camName); + hasAccess = true; + } else if (config.userPermissions.commandPriority[2] == accessProfile.accessLevel) { + logger.log("Reached Mod Access", camName); + //not directly allowed + //check if allowed access to cam + //specific mod time restrictions + if (config.timeRestrictedScenes.includes(camName)) { + //check time + let now = new Date(); + var minutes = now.getMinutes(); + var hour = now.getHours(); + if (hour >= config.restrictedHours.start && hour < config.restrictedHours.end) { + //restricted time + hasAccess = false; + } else { + //allow access to mods + hasAccess = true; + } + } else { + logger.log("Reached Mod Access", "Non time restricted", camName); + hasAccess = true; + } + } else { + //too low of permission + logger.log("Switch Cams: Too Low Permission", accessProfile, fullArgs); + validCommand = false; + invalidAccess = true; + break; + } + } + + if (hasAccess) { + camList.push(fullCamName); + } else { + validCommand = false; + invalidAccess = true; + break; + } + } + + if (!validCommand) { + logger.log("Switch Cams: Invalid Command", accessProfile, "fullargs", fullArgs); // "argslist",argsList,"camlist", camList,"obslist",obsList); + //doesnt have access to one of the cams + if (invalidAccess) { + controller.connections.twitch.send(channel, `Invalid Access`); + } else { + controller.connections.twitch.send(channel, `Invalid Command`); + } + return; + } + + let currentScene = controller.connections.obs.local.currentScene || ""; + currentScene = helper.cleanName(currentScene); + + if (currentScene == "custom" && fullArgs == "" && currentCamList.length > 0) { + camList = currentCamList; + } + //logger.log("customcams",userCommand,currentCamList,argsList,argsList.length,fullArgs,camList); + + let response = null; + if (camList.length >= 5) { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "6CamBigBorder" }, config.scenePositions["6boxbig"]); + } else if (camList.length >= 4 && userCommand == "customcamsbig") { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "4CamBigBorder" }, config.scenePositions["4boxbig"]); + } else if (camList.length >= 4) { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "4CamBorder" }, config.scenePositions["4box"]); + } else if (camList.length >= 3 && userCommand == "customcamsbig") { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "4CamBigBorder" }, config.scenePositions["3boxbig"]); + } else if (camList.length >= 3) { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "3CamBorder" }, config.scenePositions["3box"]); + } else if (camList.length >= 2 && userCommand == "customcamsbig") { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "4CamBigBorder" }, config.scenePositions["2boxbig"]); + } else if (camList.length >= 2 && userCommand == "customcamstl") { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "2CamTopleftBorder" }, config.scenePositions["2boxtl"]); + } else if (camList.length >= 2 && userCommand == "customcamstr") { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "2CamToprightBorder" }, config.scenePositions["2boxtr"]); + } else if (camList.length >= 2 && userCommand == "customcamsbl") { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "2CamBottomleftBorder" }, config.scenePositions["2boxbl"]); + } else if (camList.length >= 2 && userCommand == "customcamsbr") { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "2CamBottomrightBorder" }, config.scenePositions["2boxbr"]); + } else if (camList.length >= 2) { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "2CamBorder" }, config.scenePositions["2box"]); + } else if (camList.length >= 1) { + response = await setCustomCams(controller, obsSources, "Custom Cams", camList, { "border": "1CamBorder" }, config.scenePositions["1box"]); + } + if (response) { + await controller.connections.obs.local.setScene("Custom Cams"); + + if (!config.pauseCloudSceneChange) { + setTimeout(() => { + controller.connections.obs.cloud.setScene("Alveus Server"); + }, 500) + } + controller.connections.database["customcam"] = camList; + if (userCommand == "customcamsbig") { + controller.connections.database["customcamsbig"] = true; + } else { + controller.connections.database["customcamsbig"] = false; + } + controller.connections.database["customcamscommand"] = userCommand; + } +} + +async function setCustomCams(controller, obsSources, sceneName, camList, toggleMap, positions) { + //skip disabling anything with "overlay" in name + //toggle anything inside toggleMap off except the value + try { + let sceneItems = obsSources; //controller.connections.obs.local.sceneList || []; + let sceneMap = {}; + for (let item of sceneItems) { + let sourceName = item.sourceName || ''; + sourceName = sourceName.replaceAll(" ", ""); + sourceName = sourceName.toLowerCase(); + let sourceId = item.sceneItemId; + let visibility = item.sceneItemEnabled; + let index = item.sceneItemIndex; + let type = item.sourceType; //'OBS_SOURCE_TYPE_SCENE' + sceneMap[sourceName] = { sourceName, sourceId, index, visibility, type }; + } + let indexOrder = []; + for (let i = 0; i < camList.length; i++) { + let pos = positions[i + 1]; + if (pos == null) { + continue; + } + let cam = camList[i] || ""; + cam = cam.replaceAll(" ", ""); + cam = cam.toLowerCase(); + if (cam == "") { + continue; + } + camList[i] = cam; + + let camInfo = sceneMap[cam] || {}; + let sourceId = camInfo.sourceId || ""; + let index = camInfo.index || 0; + if (sourceId == "") { + continue; + } + indexOrder.push(index); + await controller.connections.obs.local.setSceneItemIdTransform(sceneName, sourceId, pos); + } + for (let item of sceneItems) { + let sourceName = item.sourceName || ''; + sourceName = sourceName.replaceAll(" ", ""); + sourceName = sourceName.toLowerCase(); + let sourceId = item.sceneItemId; + let visibility = item.sceneItemEnabled; + let type = item.sourceType; //'OBS_SOURCE_TYPE_SCENE' + + //toggle off anything with baseName in it, except its value + for (let baseName in toggleMap) { + let base = baseName.replaceAll(" ", ""); + base = base.toLowerCase(); + let dontSkip = toggleMap[baseName]; + dontSkip = dontSkip.replaceAll(" ", ""); + dontSkip = dontSkip.toLowerCase(); + if (sourceName.includes(base)) { + if (sourceName.includes(dontSkip)) { + //enable + await controller.connections.obs.local.setSceneItemIdEnabled(sceneName, sourceId, true); + } else { + //disable + await controller.connections.obs.local.setSceneItemIdEnabled(sceneName, sourceId, false); + } + } + } + + //only toggle scene's + if (type != "OBS_SOURCE_TYPE_SCENE" || sourceName.includes("overlay")) { + //skip over anything else with Overlay + continue; + } + + //OBS Type SCENE + if (camList.includes(sourceName) && visibility != true) { + //enable if its in the list + await controller.connections.obs.local.setSceneItemIdEnabled(sceneName, sourceId, true); + } else if (!camList.includes(sourceName) && visibility != false) { + //disable + await controller.connections.obs.local.setSceneItemIdEnabled(sceneName, sourceId, false); + } + + } + // logger.log("sceneMap",sceneMap); + //order the cam's + // indexOrder.reverse(); + // OBS Higher number = Top of list. 0 is bottom + //sort ascending order + indexOrder.sort(function (a, b) { return a - b }); + let lastindex = 0; + for (let i = 0; i < camList.length; i++) { + let name = camList[i]; + let camInfo = sceneMap[name] || {}; + let sourceId = camInfo.sourceId || ""; + if (sourceId == "") { + continue; + } + lastindex = indexOrder[i] ?? lastindex; + if (lastindex < 7) { + lastindex = 7; + } + await controller.connections.obs.local.setSceneItemIndex(sceneName, sourceId, lastindex); + } + + //toggle music based on cameras + let foundMic = false; + for (const grp in config.micGroups) { + for (const source in config.micGroups[grp]) { + if (source == "fox") { + continue; + } + let micName = source || ""; + micName = micName.toLowerCase(); + for (let i = 0; i < camList.length; i++) { + let camName = camList[i] || ""; + camName = camName.toLowerCase(); + if (micName != "" && camName != "" && camName.includes(micName)) { + foundMic = true; + break; + } + } + if (foundMic) { + break; + } + } + if (foundMic) { + break; + } + } + + if (!foundMic) { + //turn on music if no camera has a mic + controller.connections.obs.cloud.setMute(config.globalMusicSource, false); + } + logger.log(`setCustomCams - ${sceneName} (${camList})`); + return true; + } catch (e) { + logger.log(`Error setCustomCams (${sceneName},${camList}): ${JSON.stringify(e)}`); + return null; + } +} + +async function setPTZRoamMode(controller, scene) { + if (controller.connections.database[scene] == null) { + //invalid scene + return; + } + let length = controller.connections.database[scene].roamTime; + let speed = controller.connections.database[scene].roamSpeed; + let list = controller.connections.database[scene].roamList; + if (length == null || isNaN(parseInt(length)) || list == null || list.length <= 1) { + //do nothing if only 1 position or less + return; + } + let camera = controller.connections.cameras[scene] || null; + if (camera == null) { + //cant find camera client + return; + } + if (!isNaN(parseFloat(speed))) { + //set speed + camera.setSpeed(speed); + } + + if (!controller.connections.database[scene].isRoaming) { + //stop roaming + let currentSpeed = await camera.getSpeed(); + let oldSpeed = controller.connections.database[scene].speed; + if (oldSpeed != currentSpeed) { + camera.setSpeed(oldSpeed); + } + return; + } + + + let currentIndex = controller.connections.database[scene].roamIndex; + if (currentIndex == null) { + currentIndex = -1; + } + let currentDirection = controller.connections.database[scene].roamDirection || "forward"; + + if (currentDirection == "forward") { + //increment forward + currentIndex++; + if (currentIndex >= list.length) { + currentDirection = "reverse"; + currentIndex = list.length - 2 || 0; + } + } else if (currentDirection == "reverse") { + //increment reverse + currentIndex--; + if (currentIndex < 0) { + currentDirection = "forward"; + if (currentIndex < -1) { + currentIndex = 0; + } else { + currentIndex = 1; + } + + } + } + controller.connections.database[scene].roamIndex = currentIndex; + controller.connections.database[scene].roamDirection = currentDirection; + + //change position + let newPosition = list[currentIndex]; + let preset = controller.connections.database[scene].presets[newPosition]; + // logger.log("Roam preset",list,currentIndex,newPosition); + if (preset != null) { + camera.ptz({ pan: preset.pan, tilt: preset.tilt, zoom: preset.zoom, focus: preset.focus, autofocus: preset.autofocus }); + } + roamTimeout = setTimeout(setPTZRoamMode, length * 1000, controller, scene); +} + +function runAtSpecificTimeOfDay(hour, minutes, func) { + const twentyFourHours = 86400000; + const now = new Date(); + let eta_ms = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minutes, 0, 0).getTime() - now; + if (eta_ms < 0) { + eta_ms += twentyFourHours; + } + setTimeout(function () { + //run once + func(); + // run every 24 hours from now on + setInterval(func, twentyFourHours); + }, eta_ms); +} \ No newline at end of file diff --git a/src/utils/helper.js b/src/utils/helper.js new file mode 100644 index 0000000..2a24b5f --- /dev/null +++ b/src/utils/helper.js @@ -0,0 +1,103 @@ +const path = require("path"); +const config = require(path.join(__dirname, "../config/config.js")); + +//---------- Helper Functions ----------------------------------------------- +function commandCheck(message){ + message = message.trim(); + //only use first word + message = message.split(" ")[0] || message; + if (!message.startsWith(config.commandPrefix)){ + //not command + return null; + } + message = message.replace(config.commandPrefix,""); + message = message.toLowerCase(); + let convertedAlias = config.commandAliasConverted[message]; + if (convertedAlias != null){ + message = convertedAlias; + } + if (config.commandList.includes(message)){ + return message; + } + return null; +} + +function isAllowed(userCommand,userProfile){ + //user = {name,isMod,isVip,isSub}; + userCommand = userCommand || ""; + for (const permission in config.commandPermissions){ + //go through all commands + for (const command of config.commandPermissions[permission]){ + //see if in correct category + if (userCommand.toLowerCase() == command.toLowerCase()){ + //go through ranks to check if user is allowed + for (const priority of config.userPermissions.commandPriority){ + let userRank = getUserRank(userProfile); + if (config.userPermissions[priority].includes(userProfile.userName.toLowerCase())){ + return {allowed:true,accessLevel:priority}; + } else if (config.userPermissions[priority].includes("mods") && userRank >= 4){ + return {allowed:true,accessLevel:priority}; + } else if (config.userPermissions[priority].includes("vips") && userRank >= 3){ + return {allowed:true,accessLevel:priority}; + } else if (config.userPermissions[priority].includes("subs") && userRank >= 2){ + return {allowed:true,accessLevel:priority}; + } else if (config.userPermissions[priority].includes("all")){ + return {allowed:true,accessLevel:priority}; + } + if (priority == permission){ + //stop when priority rank reached + break; + } + } + } + } + } + return {allowed:false,accessLevel:null}; +} + +function getUserRank(userProfile){ + let userRank = 0; //0-5, pleb,founder,sub,vip,mod,broadcaster + if (userProfile.isBroadcaster){ + userRank = 5; + } else if (userProfile.isMod){ + userRank = 4; + } else if (userProfile.isVip){ + userRank = 3; + } else if (userProfile.isSubscriber){ + userRank = 2; + } else if (userProfile.isFounder){ + userRank = 1; + } + return userRank; +} + +function cleanName(input){ + let newInput = input; + try{ + newInput = newInput.toLowerCase(); + newInput = newInput.replaceAll(/e?s(\s|\W|$|multi(?:cam)?|cam|outdoor|indoor|wideangle|corner|den)/g, "$1"); + newInput = newInput.replaceAll(/(?:full)?cams?/g, ""); + newInput = newInput.replaceAll(" ", ""); + return newInput; + } catch(e){ + console.log(`Failed to condense input (${input}): `,e); + } + return input; +} + +/** + * Takes all functions/objects from |sourceScope| + * and adds them to |targetScope|. + */ +function importAll(sourceScope, targetScope) { + for (let name in sourceScope) { + targetScope[name] = sourceScope[name]; + } +} + +module.exports = { + commandCheck, + isAllowed, + importAll, + cleanName, +} \ No newline at end of file diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..700e0c8 --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,54 @@ +const levels = ["debug", "info", "log", "warn", "error", "silent"]; +const levelsEnum = levels.reduce((obj, l, i) => ({ ...obj, [l]: i }), {}); + +class Logger { + #context = ""; + #level = 0; + + constructor(context, level = "info") { + this.#context = context; + if (level) this.level = level; + } + + set level(level) { + if (!Object.prototype.hasOwnProperty.call(levelsEnum, level)) { + throw new Error(`Invalid log level: ${level}`); + } + this.#level = levels[level]; + } + + get level() { + return levels[this.#level]; + } + + get #prefix() { + return `[${this.#context}]`; + } + + debug(...args) { + if (this.#level < levels.debug) return; + console.debug(this.#prefix, ...args); + } + + info(...args) { + if (this.#level < levels.info) return; + console.info(this.#prefix, ...args); + } + + log(...args) { + if (this.#level < levels.log) return; + console.log(this.#prefix, ...args); + } + + warn(...args) { + if (this.#level < levels.warn) return; + console.warn(this.#prefix, ...args); + } + + error(...args) { + if (this.#level < levels.error) return; + console.error(this.#prefix, ...args); + } +} + +module.exports = Logger; diff --git a/src/utils/utilsModule.js b/src/utils/utilsModule.js new file mode 100644 index 0000000..4dc86b0 --- /dev/null +++ b/src/utils/utilsModule.js @@ -0,0 +1,236 @@ +// Utils Module +// Updated: 6/10/23 + +//const utilsModule = require(__dirname + "/utilsModule.js"); +// const utils = new utilsModule(`[OBSConnect]`); + +// import * as utils from "./utils.mjs"; +//const utils = require(__dirname + "/utils.js"); + +const fs = require("fs"); + +class UtilsModule { + log_prefix = ""; + debug_mode = true; + property = "value"; + + constructor(prefix,debugMode) { + if (prefix !== null){ + this.log_prefix = prefix; + } + if (debugMode){ + this.debug_mode = true; + } else if (debugMode === false){ + this.debug_mode = false; + } + } + + getPrefix() { + return this.log_prefix; + } + setPrefix(prefix) { + this.log_prefix = prefix; + } + getDebug() { + return this.debug_mode; + } + setDebug(boolean) { + if (boolean === true) { + this.debug_mode = true; + } else { + this.debug_mode = false; + } + } + log() { + if (this.debug_mode) { + // 1. Convert args to a normal array + var args = Array.prototype.slice.call(arguments); + // 2. Prepend log prefix log string + if (this.log_prefix != "") { + args.unshift(this.log_prefix); + } + + // 3. Pass along arguments to console.log + console.log.apply(console, args); + } + } + + logError() { + if (this.debug_mode) { + // 1. Convert args to a normal array + var args = Array.prototype.slice.call(arguments); + // 2. Prepend log prefix log string + if (this.log_prefix != "") { + args.unshift(this.log_prefix); + } + + // 3. Pass along arguments to console.log + console.error.apply(console, args); + } + } + + getNowCST(){ + let now = new Date(); + return now.toLocaleString("en-US", { timeZone: "America/Chicago" }) + } + getRandomKey(obj) { + var keys = Object.keys(obj); + return keys[Math.floor(Math.random() * keys.length)]; + } + randomIntFromInterval(min, max) { + // min and max included + return Math.floor(Math.random() * (max - min + 1) + min); + } + isNumber(n) { + return /^-?[\d.]+(?:e-?\d+)?$/.test(n); + } + sortBy(array, p) { + return array.slice(0).sort(function (a, b) { + return a[p] < b[p] ? 1 : a[p] > b[p] ? -1 : 0; + }); + } + // Array.prototype.sortBy = function (p) { + // return this.slice(0).sort(function (a, b) { + // return a[p] < b[p] ? 1 : a[p] > b[p] ? -1 : 0; + // }); + // }; + secondsToTime(given_seconds) { + const dateObj = new Date(given_seconds * 1000); + const hours = dateObj.getUTCHours(); + const minutes = dateObj.getUTCMinutes(); + const seconds = dateObj.getSeconds(); + + const timeString = + hours.toString().padStart(2, "0") + + ":" + + minutes.toString().padStart(2, "0") + + ":" + + seconds.toString().padStart(2, "0"); + return timeString; + } + + getSecondsAgoDate(secs) { + var second = 1000; + var minute = 1000 * 60; + var hour = 1000 * 60 * 60; + var day = 1000 * 60 * 60 * 24; + var week = 1000 * 60 * 60 * 24 * 7; + var year = 1000 * 60 * 60 * 24 * 365; + //1month = 2629746, 1week = 604800, 1 day = 86400, 1 hour = 3600 + let enddate = new Date(); + enddate = new Date(enddate.getTime() - secs * 1000); + return enddate; + } + + reviver(key, value) { + const dateFormat = + /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/; + if (typeof value === "string" && dateFormat.test(value)) { + return new Date(value); + } + return value; + } + + formatNumber(num, decimals) { + decimals = decimals || 0; + let number = num; + if (num == null || isNaN(num) || num == "") { + number = 0; + } + let fix = Number(Number(num).toFixed(decimals)); + return "$" + fix.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,"); + } + + //get random weighted item, options {weight,item} + weighted_random(options) { + var i; + + var weights = []; + + for (i = 0; i < options.length; i++) + weights[i] = options[i].weight + (weights[i - 1] || 0); + + var random = Math.random() * weights[weights.length - 1]; + + for (i = 0; i < weights.length; i++) if (weights[i] > random) break; + + return options[i].item; + } + + escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + } + + getFormattedTime(timeZone) { + timeZone = timeZone || "America/Chicago"; + let date = new Date().toLocaleString("en-US", { + timeZone: timeZone, + hour12: false, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + let dateString = date.match(/(\d+)\/(\d+)\/(\d+), (\d+):(\d+):(\d+)/); + let m = dateString[1]; + let d = dateString[2]; + let y = dateString[3]; + let h = dateString[4]; + let min = dateString[5]; + let s = dateString[6]; + //date = date.replace(/\s/g, "").replace(/\//g, "-").replace(",", "T").replace(/:/g, ""); + let formatted = y + "-" + m + "-" + d + "T" + h + "_" + min + "_" + s; + return formatted; + } + + loadFile(filePath, defaults){ + try{ + const jsonString = fs.readFileSync(filePath); + let file = JSON.parse(jsonString); + for (let d in defaults){ + file[d] = file[d] ?? defaults[d]; + } + this.log(`Loading file ${filePath}: `,file); + return file; + } catch (e){ + if (e.code === 'ENOENT') { + //no file + return {}; + } else { + throw `Error loading database: ${e}`; + } + } + } + + //---------------------- File management--------------------------- + + setShutdown() { + let self = this; + //-------- SHUTDOWN------------------------------------------------- + process.stdin.resume(); //so the program will not close instantly + + function exitHandler(options, exitCode) { + self.log("Closing Program", exitCode); + if (exitCode || exitCode === 0) self.log(exitCode); + if (options.exit) process.exit(); + } + //do something when app is closing + process.on("exit", exitHandler.bind(null, { exit: true })); + + //catches ctrl+c event + process.on("SIGINT", exitHandler.bind(null, { exit: true })); + process.on("SIGQUIT", exitHandler.bind(null, { exit: true })); + process.on("SIGTERM", exitHandler.bind(null, { exit: true })); + // catches "kill pid" (for example: nodemon restart) + process.on("SIGUSR1", exitHandler.bind(null, { exit: true })); + process.on("SIGUSR2", exitHandler.bind(null, { exit: true })); + + //catches uncaught exceptions + //process.on('uncaughtException', exitHandler.bind(null, { exit: false })); + } + +} + +module.exports = UtilsModule;