diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 0000000..7a94bcf --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,161 @@ +name: Package CICD +on: + push: + branches: + - '*' + +jobs: + ## + # Testing + ## + test: + name: Run Tests + runs-on: ubuntu-latest + env: + TZ: "America/Chicago" + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: setup Node + uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: 'https://npm.pkg.github.com' + - name: Cache node modules + id: cache-nodemodules + uses: actions/cache@v3 + env: + cache-name: cache-node-modules-test + with: + # caching node_modules + path: node_modules + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Install Dependencies + if: steps.cache-nodemodules.outputs.cache-hit != 'true' + run: npm ci + - name: Test + run: npm run coverage + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + lint: + name: Linting + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + - name: setup Node + uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: 'https://npm.pkg.github.com' + - name: Cache node modules + id: cache-nodemodules + uses: actions/cache@v3 + env: + cache-name: cache-node-modules-lint + with: + # caching node_modules + path: node_modules + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + - name: Install Dependencies + if: steps.cache-nodemodules.outputs.cache-hit != 'true' + run: npm ci + - name: Run lining + run: npm run lint + + ## + # Code analysis + ## + analyze: + name: Analyze + runs-on: ubuntu-latest + needs: [test, lint] + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['javascript'] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + + + ## + # Release + ## + release: + if: contains(' + refs/heads/main + refs/heads/beta + refs/heads/next + ', github.ref) + needs: [test, lint, analyze] + permissions: write-all + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + - name: Install dependencies + run: npm clean-install + - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies + run: npm audit signatures + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npx semantic-release \ No newline at end of file diff --git a/.github/workflows/codequality.yml b/.github/workflows/codequality.yml index bd15c99..70410ce 100644 --- a/.github/workflows/codequality.yml +++ b/.github/workflows/codequality.yml @@ -1,54 +1,54 @@ -name: Analyze Code -on: - push: - branches: - - '*' -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: ['javascript'] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 +#name: Analyze Code +#on: +# push: +# branches: +# - '*' +#jobs: +# analyze: +# name: Analyze +# runs-on: ubuntu-latest +# permissions: +# actions: read +# contents: read +# security-events: write +# +# strategy: +# fail-fast: false +# matrix: +# language: ['javascript'] +# # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] +# # Learn more: +# # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed +# +# steps: +# - name: Checkout repository +# uses: actions/checkout@v4 +# +# # Initializes the CodeQL tools for scanning. +# - name: Initialize CodeQL +# uses: github/codeql-action/init@v2 +# with: +# languages: ${{ matrix.language }} +# # If you wish to specify custom queries, you can do so here or in a config file. +# # By default, queries listed here will override any specified in a config file. +# # Prefix the list here with "+" to use these queries and those in the config file. +# # queries: ./path/to/local/query, your-org/your-repo/queries@main +# +# # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). +# # If this step fails, then you should remove it and run the build manually (see below) +# - name: Autobuild +# uses: github/codeql-action/autobuild@v2 +# +# # ℹī¸ Command-line programs to run using the OS shell. +# # 📚 https://git.io/JvXDl +# +# # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines +# # and modify them (or add more) to build your code if your project +# # uses a compiled language +# +# #- run: | +# # make bootstrap +# # make release +# +# - name: Perform CodeQL Analysis +# uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/semanticversionpublish.yml b/.github/workflows/semanticversionpublish.yml index bf3a230..2a6b979 100644 --- a/.github/workflows/semanticversionpublish.yml +++ b/.github/workflows/semanticversionpublish.yml @@ -1,55 +1,55 @@ -name: Semantic release versioning -on: - push: - branches: - - main - - next - - beta -jobs: - release: - permissions: write-all - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: 18 - - name: Install dependencies - run: npm clean-install - - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies - run: npm audit signatures - - name: Release - env: - GITHUB_TOKEN: ${{ secrets.PAT }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npx semantic-release -# - name: Zip artifact for release -# id: zip-dist -# run: zip dist.zip ./dist/* -r -# -# - name: Create Release -# id: create_release -# uses: actions/create-release@v1 -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token -# with: -# tag_name: ${{ github.ref_name }} -# release_name: Release ${{ github.ref_name }} -# body: Todo, get release notes! -# draft: false -# prerelease: false -# -# - name: Upload Release Build -# id: upload-release-build -# uses: actions/upload-release-asset@v1 -# env: -# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -# with: -# upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps -# asset_path: ./dist.zip -# asset_name: dist.zip -# asset_content_type: application/zip +#name: Semantic release versioning +#on: +# push: +# branches: +# - main +# - next +# - beta +#jobs: +# release: +# permissions: write-all +# runs-on: ubuntu-latest +# steps: +# - name: Checkout +# uses: actions/checkout@v4 +# with: +# fetch-depth: 0 +# - name: Setup Node.js +# uses: actions/setup-node@v3 +# with: +# node-version: 18 +# - name: Install dependencies +# run: npm clean-install +# - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies +# run: npm audit signatures +# - name: Release +# env: +# GITHUB_TOKEN: ${{ secrets.PAT }} +# NPM_TOKEN: ${{ secrets.NPM_TOKEN }} +# run: npx semantic-release +## - name: Zip artifact for release +## id: zip-dist +## run: zip dist.zip ./dist/* -r +## +## - name: Create Release +## id: create_release +## uses: actions/create-release@v1 +## env: +## GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token +## with: +## tag_name: ${{ github.ref_name }} +## release_name: Release ${{ github.ref_name }} +## body: Todo, get release notes! +## draft: false +## prerelease: false +## +## - name: Upload Release Build +## id: upload-release-build +## uses: actions/upload-release-asset@v1 +## env: +## GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +## with: +## upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps +## asset_path: ./dist.zip +## asset_name: dist.zip +## asset_content_type: application/zip diff --git a/.github/workflows/testandlint.yml b/.github/workflows/testandlint.yml index 64b1134..9a49024 100644 --- a/.github/workflows/testandlint.yml +++ b/.github/workflows/testandlint.yml @@ -1,70 +1,72 @@ -name: Test and Lint code -on: - push: - branches: - - '*' -jobs: - test: - name: Run Tests - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v4 - - - name: setup Node - uses: actions/setup-node@v3 - with: - node-version: 18 - registry-url: 'https://npm.pkg.github.com' - - name: Cache node modules - id: cache-nodemodules - uses: actions/cache@v3 - env: - cache-name: cache-node-modules - with: - # caching node_modules - path: node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - name: Install Dependencies - if: steps.cache-nodemodules.outputs.cache-hit != 'true' - run: npm ci - - name: Test - run: npm run coverage - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - lint: - name: Linting - runs-on: ubuntu-latest - steps: - - name: checkout - uses: actions/checkout@v3 - - name: setup Node - uses: actions/setup-node@v3 - with: - node-version: 18 - registry-url: 'https://npm.pkg.github.com' - - name: Cache node modules - id: cache-nodemodules - uses: actions/cache@v3 - env: - cache-name: cache-node-modules - with: - # caching node_modules - path: node_modules - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - name: Install Dependencies - if: steps.cache-nodemodules.outputs.cache-hit != 'true' - run: npm ci - - name: Run lining - run: npm run lint +#name: Test and Lint code +#on: +# push: +# branches: +# - '*' +#jobs: +# test: +# name: Run Tests +# runs-on: ubuntu-latest +# env: +# TZ: "America/Chicago" +# steps: +# - name: checkout +# uses: actions/checkout@v4 +# +# - name: setup Node +# uses: actions/setup-node@v3 +# with: +# node-version: 18 +# registry-url: 'https://npm.pkg.github.com' +# - name: Cache node modules +# id: cache-nodemodules +# uses: actions/cache@v3 +# env: +# cache-name: cache-node-modules +# with: +# # caching node_modules +# path: node_modules +# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} +# restore-keys: | +# ${{ runner.os }}-build-${{ env.cache-name }}- +# ${{ runner.os }}-build- +# ${{ runner.os }}- +# - name: Install Dependencies +# if: steps.cache-nodemodules.outputs.cache-hit != 'true' +# run: npm ci +# - name: Test +# run: npm run coverage +# - name: Upload coverage reports to Codecov +# uses: codecov/codecov-action@v3 +# env: +# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} +# +# lint: +# name: Linting +# runs-on: ubuntu-latest +# steps: +# - name: checkout +# uses: actions/checkout@v3 +# - name: setup Node +# uses: actions/setup-node@v3 +# with: +# node-version: 18 +# registry-url: 'https://npm.pkg.github.com' +# - name: Cache node modules +# id: cache-nodemodules +# uses: actions/cache@v3 +# env: +# cache-name: cache-node-modules +# with: +# # caching node_modules +# path: node_modules +# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} +# restore-keys: | +# ${{ runner.os }}-build-${{ env.cache-name }}- +# ${{ runner.os }}-build- +# ${{ runner.os }}- +# - name: Install Dependencies +# if: steps.cache-nodemodules.outputs.cache-hit != 'true' +# run: npm ci +# - name: Run lining +# run: npm run lint diff --git a/.gitignore b/.gitignore index f493dd5..e3ddf9c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ /dist .env +tests/temp .idea/ diff --git a/README.md b/README.md index bfbfc97..b88d321 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,12 @@ skeets** - [Overview](#overview) - [Agent](./src/agent/README.md) - [Validators](./src/validations/README.md) -- [Actions](./src/actions/README.md) -- [Handlers](./src/handlers/README.md) - - [Record Handlers](./src/handlers/README.md) - - [Pre-made Handlers](./src/handlers/premade-handlers/README.md) -- [Jetsteam Firehose Subscription](./src/firehose/README.md) +- [Actions](src/actions/README.md) +- [Handlers](src/handlers/message-handlers/README.md) + - [JetstreamRecord Handlers](src/handlers/message-handlers/README.md) + - [Pre-made Handlers](src/handlers/message-handlers/premade-handlers/README.md) +- [Jetsteam Firehose Subscription](src/subscriptions/firehose/README.md) +- [Interval Subscription](src/subscriptions/README.md) - [Utility Functions](./src/utils/README.md) - [Jetstream Types](./src/types/README.md) - [Credits](#credits) @@ -75,9 +76,9 @@ Initialize your handlers const handlers: JetstreamSubscriptionHandlers = { post: { c: [ - new CreateSkeetHandler( - [new InputEqualsValidator('Hello')], - [new ReplyToSkeetAction('World!')], + MessageHandler.make( + [InputEqualsValidator.make('Hello')], + [CreateSkeetAction.make('World!', MessageHandler.generateReplyFromMessage)], testAgent ), ], @@ -106,9 +107,9 @@ for our example, we'll only be acting upon post creations, so our handlers will const handlers: JetstreamSubscriptionHandlers = { post: { c: [ - new CreateSkeetHandler( - [new InputEqualsValidator('Hello')], - [new ReplyToSkeetAction('World!')], + MessageHandler.make( + [InputEqualsValidator.make('Hello')], + [ReplyToSkeetAction.make('World!', MessageHandler.generateReplyFromMessage)], testAgent ), ], @@ -153,6 +154,7 @@ import { ReplyToSkeetAction, DebugLog, } from 'bsky-event-handlers'; +import { MessageHandler } from './MessageHandler'; const testAgent = new HandlerAgent( 'test-bot', @@ -165,9 +167,9 @@ let jetstreamSubscription: JetstreamSubscription; const handlers: JetstreamSubscriptionHandlers = { post: { c: [ - new CreateSkeetHandler( - [new InputEqualsValidator('Hello')], - [new ReplyToSkeetAction('World!')], + MessageHandler.make( + [InputEqualsValidator.make('Hello')], + [ReplyToSkeetAction.make('World!', MessageHandler.generateReplyFromMessage)], testAgent ), ], @@ -204,6 +206,7 @@ TEST_BSKY_PASSWORD=app-pass-word DEBUG_LOG_ACTIVE=true #This will enable DebugLog DEBUG_LOG_LEVEL=info # This sets the minimum log level that will be output JETSTREAM_URL='ws://localhost:6008/subscribe' +SESSION_DATA_PATH="./sessionData" ``` # Overview @@ -214,7 +217,7 @@ package offers a wide array of inbuilt validators and action handlers to facilit actions- all of which contribute to smoother, faster, and more efficient bot development. The package internally uses the Bluesky Agent to interact with the Bluesky network. The flexibility provided by the -AbstractValidator and AbstractMessageAction base classes, paves the way for easy extension and creation of custom +AbstractValidator and AbstractAction base classes paves the way for easy extension and creation of custom validators and actions to suit your specific requirements. By leveraging the combination of Validators and Actions, you can create a unique sequence of automatic responses for @@ -225,9 +228,7 @@ your bot in response to defined triggers, enhancing your bot's interactivity, fl ## Packages/dependencies used - [@atproto/api](https://www.npmjs.com/package/@atproto/api) -- [Jetstream](https://github.com/ericvolp12/jetstream) (Though I use - a [forked version](https://github.com/juni-b-queer/jetstream) to include the CID and build/publish the docker - container) +- [Jetstream](https://github.com/bluesky-social/jetstream) ## Contact me diff --git a/package-lock.json b/package-lock.json index 1a71390..e945236 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,32 +9,32 @@ "version": "0.4.59", "license": "BSD-4-Clause", "dependencies": { - "@atproto/api": "^0.12.7", - "atproto-firehose": "^0.2.2", - "ws": "^8.16.0" + "@atproto/api": "^0.13.19", + "moment-timezone": "^0.5.46", + "ws": "^8.18.0" }, "devDependencies": { "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^11.1.0", "@semantic-release/exec": "^6.0.3", - "@types/jest": "^29.5.11", - "@types/ws": "^8.5.10", - "@typescript-eslint/eslint-plugin": "^7.5.0", - "@typescript-eslint/parser": "^7.5.0", + "@types/jest": "^29.5.14", + "@types/ws": "^8.5.13", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", "conventional-changelog-conventionalcommits": "^7.0.2", - "dotenv": "^16.4.5", - "eslint": "^8.56.0", + "dotenv": "^16.4.7", + "eslint": "^8.57.1", "jest": "^29.7.0", - "jest-date-mock": "^1.0.8", - "jest-mock-extended": "^3.0.5", + "jest-date-mock": "^1.0.10", + "jest-mock-extended": "^3.0.7", "jest-websocket-mock": "^2.5.0", "mock-socket": "^9.3.1", "prettier": "3.2.4", "semantic-release": "^22.0.12", - "ts-jest": "^29.1.1", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "tsup": "^8.0.2", - "typescript": "^5.3.3" + "tsup": "^8.3.5", + "typescript": "^5.7.2" } }, "node_modules/@ampproject/remapping": { @@ -51,170 +51,55 @@ } }, "node_modules/@atproto/api": { - "version": "0.12.8", - "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.12.8.tgz", - "integrity": "sha512-aNbiDuaslCxS3XyMRK40/ERerqAmk5HjQc7ivTBuPQy1Svmphl5ccnsUVxJ81xjpxjv9Fli2iPgomvRFdusuNQ==", - "dependencies": { - "@atproto/common-web": "^0.3.0", - "@atproto/lexicon": "^0.4.0", - "@atproto/syntax": "^0.3.0", - "@atproto/xrpc": "^0.5.0", + "version": "0.13.19", + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.13.19.tgz", + "integrity": "sha512-rLWQBZaOIk3ds1Fx9CwrdyX3X2GbdSEvVJ9mdSPNX40joiEaE1ljGMOcziFipbvZacXynozE4E0Sb1CgOhzfmA==", + "dependencies": { + "@atproto/common-web": "^0.3.1", + "@atproto/lexicon": "^0.4.3", + "@atproto/syntax": "^0.3.1", + "@atproto/xrpc": "^0.6.4", + "await-lock": "^2.2.2", "multiformats": "^9.9.0", - "tlds": "^1.234.0" - } - }, - "node_modules/@atproto/common": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@atproto/common/-/common-0.2.0.tgz", - "integrity": "sha512-PVYSC30pyonz2MOxuBLk27uGdwyZQ42gJfCA/NE9jLeuenVDmZnVrK5WqJ7eGg+F88rZj7NcGfRsZdP0GMykEQ==", - "dependencies": { - "@atproto/common-web": "*", - "@ipld/dag-cbor": "^7.0.3", - "cbor-x": "^1.5.1", - "multiformats": "^9.6.4", - "pino": "^8.6.1" + "tlds": "^1.234.0", + "zod": "^3.23.8" } }, "node_modules/@atproto/common-web": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.3.0.tgz", - "integrity": "sha512-67VnV6JJyX+ZWyjV7xFQMypAgDmjVaR9ZCuU/QW+mqlqI7fex2uL4Fv+7/jHadgzhuJHVd6OHOvNn0wR5WZYtA==", - "dependencies": { - "graphemer": "^1.4.0", - "multiformats": "^9.9.0", - "uint8arrays": "3.0.0", - "zod": "^3.21.4" - } - }, - "node_modules/@atproto/crypto": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.0.tgz", - "integrity": "sha512-Kj/4VgJ7hzzXvE42L0rjzP6lM0tai+OfPnP1rxJ+UZg/YUDtuewL4uapnVoWXvlNceKgaLZH98g5n9gXBVTe5Q==", - "dependencies": { - "@noble/curves": "^1.1.0", - "@noble/hashes": "^1.3.1", - "uint8arrays": "3.0.0" - } - }, - "node_modules/@atproto/identifier": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@atproto/identifier/-/identifier-0.2.1.tgz", - "integrity": "sha512-WCgDgqv3aoAZ7S0R96SsHMgzodLjy95iacD5nVDdefMJj2HkL8ygv9WCjcsBWxXcpRbKip2mcpnQhxduS6oRkw==", - "deprecated": "This package is now deprecated. Please use @atproto/syntax, which provides the same interfaces.", - "dependencies": { - "@atproto/syntax": "^0.1.1" - } - }, - "node_modules/@atproto/identifier/node_modules/@atproto/common-web": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.2.4.tgz", - "integrity": "sha512-6+DOhQcTklFmeiSkZRx6iFeqi4OFtGl4yEDGATk00q4tEcPoPvyOBtYHN6+G9lrfJIfx5RfmggamvXlJv1PxxA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.3.1.tgz", + "integrity": "sha512-N7wiTnus5vAr+lT//0y8m/FaHHLJ9LpGuEwkwDAeV3LCiPif4m/FS8x/QOYrx1PdZQwKso95RAPzCGWQBH5j6Q==", "dependencies": { "graphemer": "^1.4.0", "multiformats": "^9.9.0", "uint8arrays": "3.0.0", - "zod": "^3.21.4" - } - }, - "node_modules/@atproto/identifier/node_modules/@atproto/syntax": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.1.5.tgz", - "integrity": "sha512-pbY5lOnThoAbsmrdbN9LC/dNmckfqODJiX9zjW2t3BIHYFeGBc6w9bK3Vre8A0Hg8yWkQpv6gaBLu+ykgi2DJQ==", - "dependencies": { - "@atproto/common-web": "^0.2.3" - } - }, - "node_modules/@atproto/identity": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.0.1.tgz", - "integrity": "sha512-G1dwy+rW71v7KzRRownV398ebSMckl0VxuzpdgjaWXoD5H40hPpj2xze4H0SPcym/GaVbHDx/LXfuVOJpb7cRg==", - "dependencies": { - "@atproto/common-web": "*", - "@atproto/crypto": "*", - "axios": "^0.27.2", - "zod": "^3.21.4" + "zod": "^3.23.8" } }, "node_modules/@atproto/lexicon": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.0.tgz", - "integrity": "sha512-RvCBKdSI4M8qWm5uTNz1z3R2yIvIhmOsMuleOj8YR6BwRD+QbtUBy3l+xQ7iXf4M5fdfJFxaUNa6Ty0iRwdKqQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.3.tgz", + "integrity": "sha512-lFVZXe1S1pJP0dcxvJuHP3r/a+EAIBwwU7jUK+r8iLhIja+ml6NmYv8KeFHmIJATh03spEQ9s02duDmFVdCoXg==", "dependencies": { - "@atproto/common-web": "^0.3.0", - "@atproto/syntax": "^0.3.0", + "@atproto/common-web": "^0.3.1", + "@atproto/syntax": "^0.3.1", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", - "zod": "^3.21.4" - } - }, - "node_modules/@atproto/nsid": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@atproto/nsid/-/nsid-0.1.1.tgz", - "integrity": "sha512-fTsHfiq2NF+Zm3P1XzeBrHh9BYoy5dEzGev3sY69nhemcKnfsbj/agnIN2ekSOAlyj0Bj405/3BHQSQeiDbz1w==", - "deprecated": "This package is now deprecated. Please use @atproto/syntax, which provides the same interfaces.", - "dependencies": { - "@atproto/syntax": "^0.1.1" - } - }, - "node_modules/@atproto/nsid/node_modules/@atproto/common-web": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.2.4.tgz", - "integrity": "sha512-6+DOhQcTklFmeiSkZRx6iFeqi4OFtGl4yEDGATk00q4tEcPoPvyOBtYHN6+G9lrfJIfx5RfmggamvXlJv1PxxA==", - "dependencies": { - "graphemer": "^1.4.0", - "multiformats": "^9.9.0", - "uint8arrays": "3.0.0", - "zod": "^3.21.4" - } - }, - "node_modules/@atproto/nsid/node_modules/@atproto/syntax": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.1.5.tgz", - "integrity": "sha512-pbY5lOnThoAbsmrdbN9LC/dNmckfqODJiX9zjW2t3BIHYFeGBc6w9bK3Vre8A0Hg8yWkQpv6gaBLu+ykgi2DJQ==", - "dependencies": { - "@atproto/common-web": "^0.2.3" + "zod": "^3.23.8" } }, "node_modules/@atproto/syntax": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.0.tgz", - "integrity": "sha512-Weq0ZBxffGHDXHl9U7BQc2BFJi/e23AL+k+i5+D9hUq/bzT4yjGsrCejkjq0xt82xXDjmhhvQSZ0LqxyZ5woxA==" - }, - "node_modules/@atproto/uri": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@atproto/uri/-/uri-0.1.1.tgz", - "integrity": "sha512-6l5Q0ajGHnecOd2ACuvXVpvZGK7gE+Y78y/oq29WdjWPvTeg7fBzcXlDXcZMOvBqhDJIigmAO2YCKmxqc4wJ1g==", - "deprecated": "This package is now deprecated. Please use @atproto/syntax, which provides the same interfaces.", - "dependencies": { - "@atproto/syntax": "^0.1.1" - } - }, - "node_modules/@atproto/uri/node_modules/@atproto/common-web": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.2.4.tgz", - "integrity": "sha512-6+DOhQcTklFmeiSkZRx6iFeqi4OFtGl4yEDGATk00q4tEcPoPvyOBtYHN6+G9lrfJIfx5RfmggamvXlJv1PxxA==", - "dependencies": { - "graphemer": "^1.4.0", - "multiformats": "^9.9.0", - "uint8arrays": "3.0.0", - "zod": "^3.21.4" - } - }, - "node_modules/@atproto/uri/node_modules/@atproto/syntax": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.1.5.tgz", - "integrity": "sha512-pbY5lOnThoAbsmrdbN9LC/dNmckfqODJiX9zjW2t3BIHYFeGBc6w9bK3Vre8A0Hg8yWkQpv6gaBLu+ykgi2DJQ==", - "dependencies": { - "@atproto/common-web": "^0.2.3" - } + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.1.tgz", + "integrity": "sha512-fzW0Mg1QUOVCWUD3RgEsDt6d1OZ6DdFmbKcDdbzUfh0t4rhtRAC05KbZYmxuMPWDAiJ4BbbQ5dkAc/mNypMXkw==" }, "node_modules/@atproto/xrpc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.5.0.tgz", - "integrity": "sha512-swu+wyOLvYW4l3n+VAuJbHcPcES+tin2Lsrp8Bw5aIXIICiuFn1YMFlwK9JwVUzTH21Py1s1nHEjr4CJeElJog==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.4.tgz", + "integrity": "sha512-9ZAJ8nsXTqC4XFyS0E1Wlg7bAvonhXQNQ3Ocs1L1LIwFLXvsw/4fNpIHXxvXvqTCVeyHLbImOnE9UiO1c/qIYA==", "dependencies": { - "@atproto/lexicon": "^0.4.0", - "zod": "^3.21.4" + "@atproto/lexicon": "^0.4.3", + "zod": "^3.23.8" } }, "node_modules/@babel/code-frame": { @@ -796,78 +681,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", - "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@cbor-extract/cbor-extract-darwin-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", - "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@cbor-extract/cbor-extract-linux-arm": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", - "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@cbor-extract/cbor-extract-linux-arm64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", - "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@cbor-extract/cbor-extract-linux-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", - "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@cbor-extract/cbor-extract-win32-x64": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", - "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -901,9 +714,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", "cpu": [ "ppc64" ], @@ -913,13 +726,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", "cpu": [ "arm" ], @@ -929,13 +742,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", "cpu": [ "arm64" ], @@ -945,13 +758,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", "cpu": [ "x64" ], @@ -961,13 +774,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", "cpu": [ "arm64" ], @@ -977,13 +790,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", "cpu": [ "x64" ], @@ -993,13 +806,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", "cpu": [ "arm64" ], @@ -1009,13 +822,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", "cpu": [ "x64" ], @@ -1025,13 +838,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", "cpu": [ "arm" ], @@ -1041,13 +854,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", "cpu": [ "arm64" ], @@ -1057,13 +870,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", "cpu": [ "ia32" ], @@ -1073,13 +886,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", "cpu": [ "loong64" ], @@ -1089,13 +902,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", "cpu": [ "mips64el" ], @@ -1105,13 +918,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", "cpu": [ "ppc64" ], @@ -1121,13 +934,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", "cpu": [ "riscv64" ], @@ -1137,13 +950,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", "cpu": [ "s390x" ], @@ -1153,13 +966,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", "cpu": [ "x64" ], @@ -1169,13 +982,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", "cpu": [ "x64" ], @@ -1185,13 +998,29 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", "cpu": [ "x64" ], @@ -1201,13 +1030,13 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", "cpu": [ "x64" ], @@ -1217,13 +1046,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", "cpu": [ "arm64" ], @@ -1233,13 +1062,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", "cpu": [ "ia32" ], @@ -1249,13 +1078,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", "cpu": [ "x64" ], @@ -1265,7 +1094,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1338,21 +1167,22 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -1399,27 +1229,9 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, - "node_modules/@ipld/car": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@ipld/car/-/car-3.2.4.tgz", - "integrity": "sha512-rezKd+jk8AsTGOoJKqzfjLJ3WVft7NZNH95f0pfPbicROvzTyvHCNy567HzSUd6gRXZ9im29z5ZEv9Hw49jSYw==", - "dependencies": { - "@ipld/dag-cbor": "^7.0.0", - "multiformats": "^9.5.4", - "varint": "^6.0.0" - } - }, - "node_modules/@ipld/dag-cbor": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@ipld/dag-cbor/-/dag-cbor-7.0.3.tgz", - "integrity": "sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA==", - "dependencies": { - "cborg": "^1.6.0", - "multiformats": "^9.5.4" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1950,28 +1762,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@noble/curves": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", - "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", - "dependencies": { - "@noble/hashes": "1.4.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2250,9 +2040,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz", + "integrity": "sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ==", "cpu": [ "arm" ], @@ -2263,9 +2053,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz", + "integrity": "sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA==", "cpu": [ "arm64" ], @@ -2276,9 +2066,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz", + "integrity": "sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q==", "cpu": [ "arm64" ], @@ -2289,9 +2079,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz", + "integrity": "sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w==", "cpu": [ "x64" ], @@ -2301,10 +2091,36 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz", + "integrity": "sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz", + "integrity": "sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz", + "integrity": "sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w==", "cpu": [ "arm" ], @@ -2315,9 +2131,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", - "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz", + "integrity": "sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg==", "cpu": [ "arm" ], @@ -2328,9 +2144,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz", + "integrity": "sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg==", "cpu": [ "arm64" ], @@ -2341,9 +2157,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz", + "integrity": "sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw==", "cpu": [ "arm64" ], @@ -2354,9 +2170,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", - "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz", + "integrity": "sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ==", "cpu": [ "ppc64" ], @@ -2367,9 +2183,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz", + "integrity": "sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g==", "cpu": [ "riscv64" ], @@ -2380,9 +2196,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", - "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz", + "integrity": "sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw==", "cpu": [ "s390x" ], @@ -2393,9 +2209,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz", + "integrity": "sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw==", "cpu": [ "x64" ], @@ -2406,9 +2222,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz", + "integrity": "sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw==", "cpu": [ "x64" ], @@ -2419,9 +2235,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz", + "integrity": "sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg==", "cpu": [ "arm64" ], @@ -2432,9 +2248,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz", + "integrity": "sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A==", "cpu": [ "ia32" ], @@ -2445,9 +2261,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", - "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz", + "integrity": "sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ==", "cpu": [ "x64" ], @@ -3038,9 +2854,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@types/graceful-fs": { @@ -3077,21 +2893,15 @@ } }, "node_modules/@types/jest": { - "version": "29.5.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, "node_modules/@types/node": { "version": "20.12.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz", @@ -3107,12 +2917,6 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3120,9 +2924,9 @@ "dev": true }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", "dev": true, "dependencies": { "@types/node": "*" @@ -3144,21 +2948,19 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", - "integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/type-utils": "7.8.0", - "@typescript-eslint/utils": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", - "debug": "^4.3.4", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { @@ -3179,15 +2981,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", - "integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "engines": { @@ -3207,13 +3009,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", - "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3224,13 +3026,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", - "integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.8.0", - "@typescript-eslint/utils": "7.8.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3251,9 +3053,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", - "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3264,13 +3066,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", - "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/visitor-keys": "7.8.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3292,18 +3094,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", - "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.15", - "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.8.0", - "@typescript-eslint/types": "7.8.0", - "@typescript-eslint/typescript-estree": "7.8.0", - "semver": "^7.6.0" + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -3317,12 +3116,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", - "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3339,17 +3138,6 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -3461,6 +3249,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -3567,69 +3356,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/atproto-firehose": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/atproto-firehose/-/atproto-firehose-0.2.2.tgz", - "integrity": "sha512-re+Do5GTkqLLxQ3LLSsmIR5s9EylyNfAuXL5Twrtd20NZGnuRicDRT4D7Ul6VErdFRZWtzz4iM35Ph/xGAa05w==", - "dependencies": { - "@atproto/api": "^0.3.12", - "@atproto/common": "^0.2.0", - "@atproto/identity": "^0.0.1", - "@atproto/lexicon": "^0.1.0", - "@ipld/car": "^3.2.3", - "@ipld/dag-cbor": "^7.0.0", - "cbor": "^9.0.0", - "chalk": "^4", - "commander": "^11.0.0", - "indent-string": "^4.0.0", - "ws": "^8.13.0" - }, - "bin": { - "af": "dist/bin/cli.js" - }, - "optionalDependencies": { - "bufferutil": "^4.0.7", - "utf-8-validate": "^6.0.3" - } - }, - "node_modules/atproto-firehose/node_modules/@atproto/api": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.3.13.tgz", - "integrity": "sha512-smDlomgipca16G+jKXAZSMfsAmA5wG8WR3Z1dj29ZShVJlhs6+HHdxX7dWVDYEdSeb2rp/wyHN/tQhxGDAkz/g==", - "dependencies": { - "@atproto/common-web": "*", - "@atproto/uri": "*", - "@atproto/xrpc": "*", - "tlds": "^1.234.0", - "typed-emitter": "^2.1.0" - } - }, - "node_modules/atproto-firehose/node_modules/@atproto/lexicon": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.1.0.tgz", - "integrity": "sha512-Iy+gV9w42xLhrZrmcbZh7VFoHjXuzWvecGHIfz44owNjjv7aE/d2P5BbOX/XicSkmQ8Qkpg0BqwYDD1XBVS+DQ==", - "dependencies": { - "@atproto/common-web": "*", - "@atproto/identifier": "*", - "@atproto/nsid": "*", - "@atproto/uri": "*", - "iso-datestring-validator": "^2.2.2", - "multiformats": "^9.6.4", - "zod": "^3.14.2" - } + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true }, "node_modules/available-typed-arrays": { "version": "1.0.7", @@ -3646,14 +3377,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } + "node_modules/await-lock": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", + "license": "MIT" }, "node_modules/babel-jest": { "version": "29.7.0", @@ -3777,43 +3505,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/before-after-hook": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true }, - "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/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -3830,12 +3527,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "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.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3894,29 +3591,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3929,6 +3603,7 @@ "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", "hasInstallScript": true, "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -3937,9 +3612,9 @@ } }, "node_modules/bundle-require": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.1.0.tgz", - "integrity": "sha512-FeArRFM+ziGkRViKRnSTbHZc35dgmR9yNog05Kn0+ItI59pOAISGvnnIwW1WgFZQW59IxD9QpJnUPkdIPfZuXg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.0.0.tgz", + "integrity": "sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==", "dev": true, "dependencies": { "load-tsconfig": "^0.2.3" @@ -3948,7 +3623,7 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "peerDependencies": { - "esbuild": ">=0.17" + "esbuild": ">=0.18" } }, "node_modules/cac": { @@ -4030,58 +3705,11 @@ "cdl": "bin/cdl.js" } }, - "node_modules/cbor": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.2.tgz", - "integrity": "sha512-JPypkxsB10s9QOWwa6zwPzqE1Md3vqpPc+cai4sAecuCsRyAtAl/pMyhPlMbT/xtPnm2dznJZYRLui57qiRhaQ==", - "dependencies": { - "nofilter": "^3.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/cbor-extract": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", - "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.1.1" - }, - "bin": { - "download-cbor-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", - "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", - "@cbor-extract/cbor-extract-linux-arm": "2.2.0", - "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", - "@cbor-extract/cbor-extract-linux-x64": "2.2.0", - "@cbor-extract/cbor-extract-win32-x64": "2.2.0" - } - }, - "node_modules/cbor-x": { - "version": "1.5.9", - "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.5.9.tgz", - "integrity": "sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ==", - "optionalDependencies": { - "cbor-extract": "^2.2.0" - } - }, - "node_modules/cborg": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/cborg/-/cborg-1.10.2.tgz", - "integrity": "sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==", - "bin": { - "cborg": "cli.js" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4103,39 +3731,18 @@ } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", "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" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/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/ci-info": { @@ -4217,6 +3824,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4227,26 +3835,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "engines": { - "node": ">=16" - } + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/compare-func": { "version": "2.0.0", @@ -4274,6 +3864,15 @@ "proto-list": "~1.2.1" } }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/conventional-changelog-angular": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", @@ -4503,12 +4102,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -4591,29 +4190,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "dev": true }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -4678,9 +4260,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, "engines": { "node": ">=12" @@ -4740,6 +4322,21 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.758", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.758.tgz", @@ -5051,41 +4648,42 @@ } }, "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" } }, "node_modules/escalade": { @@ -5110,16 +4708,17 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -5286,22 +4885,6 @@ "node": ">=0.10.0" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -5396,14 +4979,6 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, - "node_modules/fast-redact": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", - "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", - "engines": { - "node": ">=6" - } - }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -5443,16 +5018,37 @@ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=10" } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "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" @@ -5524,25 +5120,6 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -5580,19 +5157,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "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/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -5969,6 +5533,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -6110,25 +5675,6 @@ "node": ">=10.17.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -6209,6 +5755,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, "engines": { "node": ">=8" } @@ -6311,18 +5858,6 @@ "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", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", @@ -6727,6 +6262,46 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/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/jake/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/java-properties": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", @@ -7485,9 +7060,9 @@ } }, "node_modules/lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, "engines": { "node": ">=14" @@ -7773,25 +7348,6 @@ "node": ">=16" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -7843,10 +7399,29 @@ "node": ">= 8" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.46", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz", + "integrity": "sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/multiformats": { @@ -7903,26 +7478,13 @@ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", "optional": true, + "peer": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", - "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7935,14 +7497,6 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, - "node_modules/nofilter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", - "engines": { - "node": ">=12.19" - } - }, "node_modules/normalize-package-data": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.1.tgz", @@ -10667,14 +10221,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -10913,9 +10459,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -10939,41 +10485,6 @@ "node": ">=4" } }, - "node_modules/pino": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", - "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^1.2.0", - "pino-std-serializers": "^6.0.0", - "process-warning": "^3.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^3.7.0", - "thread-stream": "^2.6.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", - "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", - "dependencies": { - "readable-stream": "^4.0.0", - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", - "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" - }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -11137,9 +10648,9 @@ } }, "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", "dev": true, "funding": [ { @@ -11152,21 +10663,28 @@ } ], "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" + "lilconfig": "^3.1.1" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "peerDependencies": { + "jiti": ">=1.21.0", "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { + "jiti": { + "optional": true + }, "postcss": { "optional": true }, - "ts-node": { + "tsx": { + "optional": true + }, + "yaml": { "optional": true } } @@ -11221,25 +10739,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "node_modules/process-warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", - "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==" - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -11304,11 +10809,6 @@ } ] }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" - }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -11417,39 +10917,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/readable-stream": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", - "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "engines": { - "node": ">= 12.13.0" + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/redeyed": { @@ -11582,12 +11060,12 @@ } }, "node_modules/rollup": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", - "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.0.tgz", + "integrity": "sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ==", "dev": true, "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -11597,22 +11075,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.2", - "@rollup/rollup-android-arm64": "4.17.2", - "@rollup/rollup-darwin-arm64": "4.17.2", - "@rollup/rollup-darwin-x64": "4.17.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", - "@rollup/rollup-linux-arm-musleabihf": "4.17.2", - "@rollup/rollup-linux-arm64-gnu": "4.17.2", - "@rollup/rollup-linux-arm64-musl": "4.17.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", - "@rollup/rollup-linux-riscv64-gnu": "4.17.2", - "@rollup/rollup-linux-s390x-gnu": "4.17.2", - "@rollup/rollup-linux-x64-gnu": "4.17.2", - "@rollup/rollup-linux-x64-musl": "4.17.2", - "@rollup/rollup-win32-arm64-msvc": "4.17.2", - "@rollup/rollup-win32-ia32-msvc": "4.17.2", - "@rollup/rollup-win32-x64-msvc": "4.17.2", + "@rollup/rollup-android-arm-eabi": "4.28.0", + "@rollup/rollup-android-arm64": "4.28.0", + "@rollup/rollup-darwin-arm64": "4.28.0", + "@rollup/rollup-darwin-x64": "4.28.0", + "@rollup/rollup-freebsd-arm64": "4.28.0", + "@rollup/rollup-freebsd-x64": "4.28.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.0", + "@rollup/rollup-linux-arm-musleabihf": "4.28.0", + "@rollup/rollup-linux-arm64-gnu": "4.28.0", + "@rollup/rollup-linux-arm64-musl": "4.28.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.0", + "@rollup/rollup-linux-riscv64-gnu": "4.28.0", + "@rollup/rollup-linux-s390x-gnu": "4.28.0", + "@rollup/rollup-linux-x64-gnu": "4.28.0", + "@rollup/rollup-linux-x64-musl": "4.28.0", + "@rollup/rollup-win32-arm64-msvc": "4.28.0", + "@rollup/rollup-win32-ia32-msvc": "4.28.0", + "@rollup/rollup-win32-x64-msvc": "4.28.0", "fsevents": "~2.3.2" } }, @@ -11639,15 +11119,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "optional": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safe-array-concat": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", @@ -11666,25 +11137,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/safe-regex-test": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", @@ -11702,14 +11154,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", - "engines": { - "node": ">=10" - } - }, "node_modules/semantic-release": { "version": "22.0.12", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.12.tgz", @@ -11961,9 +11405,9 @@ } }, "node_modules/semver": { - "version": "7.6.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.1.tgz", - "integrity": "sha512-f/vbBsu+fOiYt+lmwZV0rVwJScl46HppnOA1ZvIuBWKOTlllpyJ3bfVax76/OrhCH38dyxoDIA8K7uB963IYgA==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -12200,14 +11644,6 @@ "node": ">=8" } }, - "node_modules/sonic-boom": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", - "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12269,6 +11705,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, "engines": { "node": ">= 10.x" } @@ -12346,14 +11783,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -12557,6 +11986,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -12715,14 +12145,6 @@ "node": ">=0.8" } }, - "node_modules/thread-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", - "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", - "dependencies": { - "real-require": "^0.2.0" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -12775,6 +12197,51 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/tinyexec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", + "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "dev": true + }, + "node_modules/tinyglobby": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", + "integrity": "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==", + "dev": true, + "dependencies": { + "fdir": "^6.4.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tlds": { "version": "1.252.0", "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.252.0.tgz", @@ -12878,28 +12345,30 @@ "dev": true }, "node_modules/ts-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", - "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", "dev": true, "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.1" + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" }, "engines": { - "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", @@ -12909,6 +12378,9 @@ "@babel/core": { "optional": true }, + "@jest/transform": { + "optional": true + }, "@jest/types": { "optional": true }, @@ -12963,31 +12435,27 @@ } } }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "optional": true - }, "node_modules/tsup": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.0.2.tgz", - "integrity": "sha512-NY8xtQXdH7hDUAZwcQdY/Vzlw9johQsaqf7iwZ6g1DOUlFYQ5/AtVAjTvihhEyeRlGo4dLRVHtrRaL35M1daqQ==", - "dev": true, - "dependencies": { - "bundle-require": "^4.0.0", - "cac": "^6.7.12", - "chokidar": "^3.5.1", - "debug": "^4.3.1", - "esbuild": "^0.19.2", - "execa": "^5.0.0", - "globby": "^11.0.3", - "joycon": "^3.0.1", - "postcss-load-config": "^4.0.1", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.3.5.tgz", + "integrity": "sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA==", + "dev": true, + "dependencies": { + "bundle-require": "^5.0.0", + "cac": "^6.7.14", + "chokidar": "^4.0.1", + "consola": "^3.2.3", + "debug": "^4.3.7", + "esbuild": "^0.24.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", - "rollup": "^4.0.2", + "rollup": "^4.24.0", "source-map": "0.8.0-beta.0", - "sucrase": "^3.20.3", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.1", + "tinyglobby": "^0.2.9", "tree-kill": "^1.2.2" }, "bin": { @@ -13145,14 +12613,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typed-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-2.1.0.tgz", - "integrity": "sha512-g/KzbYKbH5C2vPkaXGu8DJlHrGKHLsM25Zg9WuC9pMGfuvT+X25tZQWo5fK1BjBm8+UrVE9LDCvaY0CQk+fXDA==", - "optionalDependencies": { - "rxjs": "*" - } - }, "node_modules/typedarray.prototype.slice": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.3.tgz", @@ -13174,9 +12634,9 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -13333,6 +12793,7 @@ "integrity": "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==", "hasInstallScript": true, "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -13376,11 +12837,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/varint": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", - "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==" - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -13527,9 +12983,9 @@ } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, @@ -13570,18 +13026,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", - "dev": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -13631,9 +13075,9 @@ } }, "node_modules/zod": { - "version": "3.23.7", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.7.tgz", - "integrity": "sha512-NBeIoqbtOiUMomACV/y+V3Qfs9+Okr18vR5c/5pHClPpufWOrsx8TENboDPe265lFdfewX2yBtNTLPvnmCxwog==", + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index a3070bb..72b4337 100644 --- a/package.json +++ b/package.json @@ -33,29 +33,29 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^11.1.0", "@semantic-release/exec": "^6.0.3", - "@types/jest": "^29.5.11", - "@types/ws": "^8.5.10", - "@typescript-eslint/eslint-plugin": "^7.5.0", - "@typescript-eslint/parser": "^7.5.0", + "@types/jest": "^29.5.14", + "@types/ws": "^8.5.13", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", "conventional-changelog-conventionalcommits": "^7.0.2", - "dotenv": "^16.4.5", - "eslint": "^8.56.0", + "dotenv": "^16.4.7", + "eslint": "^8.57.1", "jest": "^29.7.0", - "jest-date-mock": "^1.0.8", - "jest-mock-extended": "^3.0.5", + "jest-date-mock": "^1.0.10", + "jest-mock-extended": "^3.0.7", "jest-websocket-mock": "^2.5.0", "mock-socket": "^9.3.1", "prettier": "3.2.4", "semantic-release": "^22.0.12", - "ts-jest": "^29.1.1", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "tsup": "^8.0.2", - "typescript": "^5.3.3" + "tsup": "^8.3.5", + "typescript": "^5.7.2" }, "dependencies": { - "@atproto/api": "^0.12.7", - "atproto-firehose": "^0.2.2", - "ws": "^8.16.0" + "@atproto/api": "^0.13.19", + "moment-timezone": "^0.5.46", + "ws": "^8.18.0" }, "release": { "plugins": [ @@ -84,6 +84,10 @@ "type": "minor", "release": "minor" }, + { + "type": "patch", + "release": "patch" + }, { "type": "fix", "release": "patch" @@ -100,6 +104,10 @@ "type": "refactor", "release": "patch" }, + { + "type": "reorganize", + "release": "patch" + }, { "type": "test", "release": "patch" @@ -127,6 +135,10 @@ "type": "minor", "section": "General Updates" }, + { + "type": "patch", + "section": "Patches" + }, { "type": "fix", "section": "Fixes" @@ -143,6 +155,10 @@ "type": "refactor", "section": "Refactoring" }, + { + "type": "reorganize", + "section": "Reorganizing" + }, { "type": "test", "section": "Testing" @@ -177,6 +193,10 @@ "type": "minor", "section": "General Updates" }, + { + "type": "patch", + "section": "Patches" + }, { "type": "fix", "section": "Fixes" @@ -193,6 +213,10 @@ "type": "refactor", "section": "Refactoring" }, + { + "type": "reorganize", + "section": "Reorganizing" + }, { "type": "test", "section": "Testing" diff --git a/src/actions/AbstractAction.ts b/src/actions/AbstractAction.ts new file mode 100644 index 0000000..65cf179 --- /dev/null +++ b/src/actions/AbstractAction.ts @@ -0,0 +1,36 @@ +import { HandlerAgent } from '../agent/HandlerAgent'; +import { DebugLog } from '../utils/DebugLog'; + +export abstract class AbstractAction { + constructor() {} + + static make(...args: any): AbstractAction { + throw new Error('Method not implemented! Use constructor!'); + } + + static getStringOrFunctionReturn( + stringOrFunction: + | string + | ((arg0: HandlerAgent, ...args: any) => string), + handlerAgent: HandlerAgent, + ...args: any + ): string { + if (typeof stringOrFunction == 'function') { + return stringOrFunction(handlerAgent, ...args); + } else { + return stringOrFunction; + } + } + + // @ts-ignore + abstract async handle( + handlerAgent: HandlerAgent, + ...args: any + ): Promise; +} + +export class TestAction extends AbstractAction { + async handle(handlerAgent: HandlerAgent): Promise { + DebugLog.info('Working', 'working'); + } +} diff --git a/src/actions/AbstractMessageAction.ts b/src/actions/AbstractMessageAction.ts deleted file mode 100644 index fb9ebc6..0000000 --- a/src/actions/AbstractMessageAction.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { RepoOp } from '@atproto/api/dist/client/types/com/atproto/sync/subscribeRepos'; -import { HandlerAgent } from '../agent/HandlerAgent'; -import { JetstreamMessage } from '../types/JetstreamTypes'; -import { DebugLog } from '../utils/DebugLog'; - -export abstract class AbstractMessageAction { - constructor() {} - - static make(...args: any): AbstractMessageAction { - throw new Error('Method not implemented! Use constructor!'); - } - - // @ts-ignore - abstract async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent - ): Promise; -} diff --git a/src/actions/FunctionAction.ts b/src/actions/FunctionAction.ts index 96c03ec..b5bee51 100644 --- a/src/actions/FunctionAction.ts +++ b/src/actions/FunctionAction.ts @@ -1,27 +1,20 @@ import { HandlerAgent } from '../agent/HandlerAgent'; -import { AbstractMessageAction } from './AbstractMessageAction'; -import { JetstreamMessage } from '../types/JetstreamTypes'; +import { AbstractAction } from './AbstractAction'; -export class FunctionAction extends AbstractMessageAction { +export class FunctionAction extends AbstractAction { constructor( - private actionFunction: ( - arg0: JetstreamMessage, - arg1: HandlerAgent - ) => any + private actionFunction: (arg0: HandlerAgent, ...args: any) => any ) { super(); } static make( - actionFunction: (arg0: JetstreamMessage, arg1: HandlerAgent) => any + actionFunction: (arg0: HandlerAgent, ...args: any) => any ): FunctionAction { return new FunctionAction(actionFunction); } - async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent - ): Promise { - await this.actionFunction(message, handlerAgent); + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { + await this.actionFunction(handlerAgent, ...args); } } diff --git a/src/actions/LoggingActions.ts b/src/actions/LoggingActions.ts index 551281b..6baa9f2 100644 --- a/src/actions/LoggingActions.ts +++ b/src/actions/LoggingActions.ts @@ -1,61 +1,63 @@ import { HandlerAgent } from '../agent/HandlerAgent'; -import { JetstreamMessage } from '../types/JetstreamTypes'; -import { AbstractMessageAction } from './AbstractMessageAction'; import { DebugLog } from '../utils/DebugLog'; +import { AbstractAction } from './AbstractAction'; -export class LogMessageAction extends AbstractMessageAction { - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any - - static make(): LogMessageAction { - return new LogMessageAction(); - } - async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent - ): Promise { - console.log(message); - } -} - -export class LogInputTextAction extends AbstractMessageAction { - constructor(private logText: string) { +export class LogInputTextAction extends AbstractAction { + constructor( + private logText: string | ((arg0: HandlerAgent, ...args: any) => string) + ) { super(); } - static make(logText: string): LogInputTextAction { + static make( + logText: string | ((arg0: HandlerAgent, ...args: any) => string) + ): LogInputTextAction { return new LogInputTextAction(logText); } // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any - async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent - ): Promise { - console.log(this.logText); + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { + const text: string = AbstractAction.getStringOrFunctionReturn( + this.logText, + handlerAgent, + ...args + ); + + console.log(text); } } -export class DebugLogAction extends AbstractMessageAction { +export class DebugLogAction extends AbstractAction { constructor( - private action: string, - private message: string, + private action: string | ((arg0: HandlerAgent, ...args: any) => string), + private message: + | string + | ((arg0: HandlerAgent, ...args: any) => string), private level: string = 'info' ) { super(); } static make( - action: string, - message: string, + action: string | ((arg0: HandlerAgent, ...args: any) => string), + message: string | ((arg0: HandlerAgent, ...args: any) => string), level: string = 'info' ): DebugLogAction { return new DebugLogAction(action, message, level); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any - async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent - ): Promise { - DebugLog.log(this.action, this.message, this.level); + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { + DebugLog.log( + AbstractAction.getStringOrFunctionReturn( + this.action, + handlerAgent, + ...args + ), + AbstractAction.getStringOrFunctionReturn( + this.message, + handlerAgent, + ...args + ), + this.level + ); } } diff --git a/src/actions/README.md b/src/actions/README.md index 9cf550e..c99b1b1 100644 --- a/src/actions/README.md +++ b/src/actions/README.md @@ -29,8 +29,8 @@ export class ExampleAction extends AbstractMessageAction { } async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamMessage ): Promise { // Perform your actions here } @@ -46,8 +46,8 @@ export class ExampleAction extends AbstractMessageAction { } async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamMessage ): Promise { // use this.userDid to access the property // Perform your actions here @@ -59,7 +59,7 @@ export class ExampleAction extends AbstractMessageAction { The `FunctionAction` class takes a function as an argument. This function gets executed when the handle method is called and it should accept `JeststreamMessage` and `HandlerAgent` as arguments. -`FunctionAction.make((message, handlerAgent) => { // Function implementation goes here });` +`FunctionAction.make((handlerAgent, message) => { // Function implementation goes here });` ## Logging Actions @@ -83,6 +83,8 @@ The `DebugLogAction` class will output to the log using the DebugLog class. give ## Skeet Actions +### These are older functions, Use actions in [standard-bsky-actions](./standard-bsky-actions/README.md) instead! + ### CreateSkeetAction Pass in a string, and when the validations pass, it will create a new skeet from the agent with the given input text. @@ -94,14 +96,14 @@ Pass in a string, and when the validations pass, it will create a new skeet from The `CreateSkeetWithGeneratedTextAction` accepts a function with 2 arguments, `JetstreamMessage` and `HandlerAgent`. This function should return a string When the validations pass, it will call the function to generate the response text -`CreateSkeetWithGeneratedTextAction.make((message: JetstreamMessage, handlerAgent) => { // Function implementation goes here });` +`CreateSkeetWithGeneratedTextAction.make((handlerAgent, message: JetstreamMessage) => { // Function implementation goes here });` ### ReplyToSkeetAction The `ReplyToSkeetAction` only works on post creation messages for now. Pass in a string, and when the validations pass, it will reply to the created skeet with a new skeet using the given input text -`ReplyToSkeetAction.make("Reply Text")` +`ReplyToSkeetAction.make("JetstreamReply Text")` ### ReplyToSkeetWithGeneratedTextAction @@ -109,4 +111,4 @@ The `ReplyToSkeetWithGeneratedTextAction` only works on post creation messages f Similar to the CreateSkeetWithGeneratedTextAction, it accepts a function with 2 arguments, but the first is a `CreateSkeetMessage` and the second is the same, being a `HandlerAgent`. This function should return a string When the validations pass, it will call the function to generate the response text -`ReplyToSkeetWithGeneratedTextAction.make((message: CreateSkeetMessage, handlerAgent) => { // Function implementation goes here });` +`ReplyToSkeetWithGeneratedTextAction.make((handlerAgent, message: CreateSkeetMessage) => { // Function implementation goes here });` diff --git a/src/actions/TestAction.ts b/src/actions/TestAction.ts deleted file mode 100644 index be00f38..0000000 --- a/src/actions/TestAction.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { HandlerAgent } from '../agent/HandlerAgent'; -import { JetstreamMessage } from '../types/JetstreamTypes'; -import { DebugLog } from '../utils/DebugLog'; -import { AbstractMessageAction } from './AbstractMessageAction'; - -export class TestAction extends AbstractMessageAction { - async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent - ): Promise { - DebugLog.info('Working', 'working'); - } -} diff --git a/src/actions/message-actions/AbstractMessageAction.ts b/src/actions/message-actions/AbstractMessageAction.ts new file mode 100644 index 0000000..4295335 --- /dev/null +++ b/src/actions/message-actions/AbstractMessageAction.ts @@ -0,0 +1,19 @@ +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { JetstreamEventCommit } from '../../types/JetstreamTypes'; +import { AbstractAction } from '../AbstractAction'; + +export abstract class AbstractMessageAction extends AbstractAction { + constructor() { + super(); + } + + static make(...args: any): AbstractMessageAction { + throw new Error('Method not implemented! Use constructor!'); + } + + // @ts-ignore + abstract async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise; +} diff --git a/src/actions/message-actions/MessageLoggingActions.ts b/src/actions/message-actions/MessageLoggingActions.ts new file mode 100644 index 0000000..b2834dc --- /dev/null +++ b/src/actions/message-actions/MessageLoggingActions.ts @@ -0,0 +1,17 @@ +import { AbstractMessageAction } from './AbstractMessageAction'; +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { JetstreamEventCommit } from '../../types/JetstreamTypes'; + +export class LogMessageAction extends AbstractMessageAction { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + + static make(): LogMessageAction { + return new LogMessageAction(); + } + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + console.log(message); + } +} diff --git a/src/actions/message-actions/TestAction.ts b/src/actions/message-actions/TestAction.ts new file mode 100644 index 0000000..76ff740 --- /dev/null +++ b/src/actions/message-actions/TestAction.ts @@ -0,0 +1,13 @@ +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { JetstreamEventCommit } from '../../types/JetstreamTypes'; +import { DebugLog } from '../../utils/DebugLog'; +import { AbstractMessageAction } from './AbstractMessageAction'; + +export class TestMessageAction extends AbstractMessageAction { + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + DebugLog.info('Working', 'working'); + } +} diff --git a/src/actions/post/SkeetActions.ts b/src/actions/message-actions/post/SkeetMessageActions.ts similarity index 53% rename from src/actions/post/SkeetActions.ts rename to src/actions/message-actions/post/SkeetMessageActions.ts index b542a71..3f8748c 100644 --- a/src/actions/post/SkeetActions.ts +++ b/src/actions/message-actions/post/SkeetMessageActions.ts @@ -1,54 +1,55 @@ import { AbstractMessageAction } from '../AbstractMessageAction'; import { - CreateSkeetMessage, - JetstreamMessage, - Reply, - Subject, -} from '../../types/JetstreamTypes'; -import { HandlerAgent } from '../../agent/HandlerAgent'; -import * as repl from 'repl'; + JetstreamEventCommit, + JetstreamReply, +} from '../../../types/JetstreamTypes'; +import { HandlerAgent } from '../../../agent/HandlerAgent'; +import { CreateSkeetAction } from '../../standard-bsky-actions/SkeetActions'; -export class CreateSkeetAction extends AbstractMessageAction { +export class CreateSkeetMessageAction extends AbstractMessageAction { constructor(private skeetText: string) { super(); } - static make(skeetText: string): CreateSkeetAction { - return new CreateSkeetAction(skeetText); + static make(skeetText: string): CreateSkeetMessageAction { + return new CreateSkeetMessageAction(skeetText); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { - await handlerAgent.createSkeet(this.skeetText); + await CreateSkeetAction.make(this.skeetText).handle(handlerAgent); } } export class CreateSkeetWithGeneratedTextAction extends AbstractMessageAction { constructor( private textGenerator: ( - arg0: JetstreamMessage, - arg1: HandlerAgent + arg0: HandlerAgent, + arg1: JetstreamEventCommit ) => string ) { super(); } static make( - textGenerator: (arg0: JetstreamMessage, arg1: HandlerAgent) => string + textGenerator: ( + arg0: HandlerAgent, + arg1: JetstreamEventCommit + ) => string ): CreateSkeetWithGeneratedTextAction { return new CreateSkeetWithGeneratedTextAction(textGenerator); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { await handlerAgent.createSkeet( - this.textGenerator(message, handlerAgent) + this.textGenerator(handlerAgent, message) ); } } @@ -64,10 +65,11 @@ export class ReplyToSkeetAction extends AbstractMessageAction { // eslint-disable-next-line @typescript-eslint/no-unused-vars async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { - const reply: Reply = handlerAgent.generateReplyFromMessage(message); + const reply: JetstreamReply = + handlerAgent.generateReplyFromMessage(message); await handlerAgent.createSkeet(this.replyText, reply); } } @@ -75,27 +77,31 @@ export class ReplyToSkeetAction extends AbstractMessageAction { export class ReplyToSkeetWithGeneratedTextAction extends AbstractMessageAction { constructor( private textGenerator: ( - arg0: CreateSkeetMessage, - arg1: HandlerAgent + arg0: HandlerAgent, + arg1: JetstreamEventCommit ) => string ) { super(); } static make( - textGenerator: (arg0: CreateSkeetMessage, arg1: HandlerAgent) => string + textGenerator: ( + arg0: HandlerAgent, + arg1: JetstreamEventCommit + ) => string ): ReplyToSkeetWithGeneratedTextAction { return new ReplyToSkeetWithGeneratedTextAction(textGenerator); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { - const reply: Reply = handlerAgent.generateReplyFromMessage(message); + const reply: JetstreamReply = + handlerAgent.generateReplyFromMessage(message); await handlerAgent.createSkeet( - this.textGenerator(message, handlerAgent), + this.textGenerator(handlerAgent, message), reply ); } diff --git a/src/actions/standard-bsky-actions/FollowActions.ts b/src/actions/standard-bsky-actions/FollowActions.ts new file mode 100644 index 0000000..3bfefae --- /dev/null +++ b/src/actions/standard-bsky-actions/FollowActions.ts @@ -0,0 +1,56 @@ +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { AbstractAction } from '../AbstractAction'; + +export class CreateFollowAction extends AbstractAction { + constructor( + protected userDid: + | string + | ((arg0: HandlerAgent, ...args: any) => string) + ) { + super(); + } + + static make( + userDid: string | ((arg0: HandlerAgent, ...args: any) => string) + ): CreateFollowAction { + return new CreateFollowAction(userDid); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { + const did: string = AbstractAction.getStringOrFunctionReturn( + this.userDid, + handlerAgent, + ...args + ); + + await handlerAgent.followUser(did); + } +} + +export class DeleteFollowAction extends AbstractAction { + constructor( + protected userDid: + | string + | ((arg0: HandlerAgent, ...args: any) => string) + ) { + super(); + } + + static make( + userDid: string | ((arg0: HandlerAgent, ...args: any) => string) + ): DeleteFollowAction { + return new DeleteFollowAction(userDid); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { + const did: string = AbstractAction.getStringOrFunctionReturn( + this.userDid, + handlerAgent, + ...args + ); + + await handlerAgent.unfollowUser(did); + } +} diff --git a/src/actions/standard-bsky-actions/LikeActions.ts b/src/actions/standard-bsky-actions/LikeActions.ts new file mode 100644 index 0000000..bde161f --- /dev/null +++ b/src/actions/standard-bsky-actions/LikeActions.ts @@ -0,0 +1,64 @@ +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { AbstractAction } from '../AbstractAction'; + +export class CreateLikeAction extends AbstractAction { + constructor( + protected skeetUri: + | string + | ((arg0: HandlerAgent, ...args: any) => string), + protected skeetCid: + | string + | ((arg0: HandlerAgent, ...args: any) => string) + ) { + super(); + } + + static make( + skeetUri: string | ((arg0: HandlerAgent, ...args: any) => string), + skeetCid: string | ((arg0: HandlerAgent, ...args: any) => string) + ): CreateLikeAction { + return new CreateLikeAction(skeetUri, skeetCid); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { + const uri: string = AbstractAction.getStringOrFunctionReturn( + this.skeetUri, + handlerAgent, + ...args + ); + const cid: string = AbstractAction.getStringOrFunctionReturn( + this.skeetCid, + handlerAgent, + ...args + ); + + await handlerAgent.likeSkeet(uri, cid); + } +} + +export class DeleteLikeAction extends AbstractAction { + constructor( + protected skeetUri: + | string + | ((arg0: HandlerAgent, ...args: any) => string) + ) { + super(); + } + + static make( + skeetUri: string | ((arg0: HandlerAgent, ...args: any) => string) + ): DeleteLikeAction { + return new DeleteLikeAction(skeetUri); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { + const uri: string = AbstractAction.getStringOrFunctionReturn( + this.skeetUri, + handlerAgent, + ...args + ); + await handlerAgent.unlikeSkeet(uri); + } +} diff --git a/src/actions/standard-bsky-actions/README.md b/src/actions/standard-bsky-actions/README.md new file mode 100644 index 0000000..b40369b --- /dev/null +++ b/src/actions/standard-bsky-actions/README.md @@ -0,0 +1,85 @@ +# Standard Actions + +Actions are the set of operations that are executed in response to certain validation or criteria fulfillment. This could range from sending reply posts, logging particular information, or executing any function, to more complex sequences of operations. You even have the ability to create custom actions based on your needs. + +These are standardized actions that make them easy to use from any subscriber or handler. +## Provided Actions + +- [Skeet Actions](#skeet-actions) + - [CreateSkeetAction](#createskeetaction) + - [DeleteSkeetAction](#deleteskeetaction) +- [Follow Actions](#follow-actions) + - [CreateFollowAction](#createfollowaction) + - [DeleteFollowAction](#deletefollowaction) +- [Like Actions](#like-actions) + - [CreateLikeAction](#createlikeaction) + - [DeleteLikeAction](#deletelikeaction) +- [Reskeet Actions](#reskeet-actions) + - [CreateReskeetAction](#createreskeetaction) + - [DeleteReskeetAction](#deletereskeetaction) + +## Skeet Actions + +### CreateSkeetAction + +Create a skeet. Accepts a string or function that returns a string to be used as the post text. + +An optional second argument can be passed in to make it a reply. A helper function `MessageHandler.generateReplyFromMessage` can be used to automatically generate the reply for a given message. +``` +CreateSkeetAction.make((handler: HandlerAgent, event: JetstreamEventCommit): string =>{ + return "hello!"; + }, + MessageHandler.generateReplyFromMessage) +``` + +### DeleteSkeetAction + +Deletes a given skeet. Accepts a string or function that returns a string that should be the uri of the skeet to delete + +`DeleteSkeetAction.make('skeetUri')` + +## Follow Actions + +### CreateFollowAction + +Follow a user. Accepts a string or function that returns a string that should be the did of the user to follow +`CreateFollowAction.make('userDid')` + +### DeleteFollowAction + +Unfollow a user. Accepts a string or function that returns a string that should be the did of the user to unfollow +`DeleteFollowAction.make('userDid')` + +## Like Actions + +### CreateLikeAction + +Like a given post, accepts a function or string for the URI and CID of the post to like. + +Helper functions `MessageHandler.getUriFromMessage` and `MessageHandler.getCidFromMessage` are provided to like a post from a given Jetstream event + +`CreateLikeAction.make(MessageHandler.getUriFromMessage, MessageHandler.getCidFromMessage)` + +### DeleteLikeAction + +Unlikes a given post. Accepts a function or string for the URI of the post to unlike + +`DeleteLikeAction.make(MessageHandler.getUriFromMessage)` + + +## Reskeet Actions + +### CreateReskeetAction + +Reskeet a given post, accepts a function or string for the URI and CID of the post to like. + +Helper functions `MessageHandler.getUriFromMessage` and `MessageHandler.getCidFromMessage` are provided to reskeet a post from a given Jetstream event + +`CreateReskeetAction.make(MessageHandler.getUriFromMessage, MessageHandler.getCidFromMessage)` + +### DeleteReskeetAction + +Unreskeets a given post. Accepts a function or string for the URI of the post to unreskeet + +`DeleteReskeetAction.make(MessageHandler.getUriFromMessage)` + diff --git a/src/actions/standard-bsky-actions/ReskeetActions.ts b/src/actions/standard-bsky-actions/ReskeetActions.ts new file mode 100644 index 0000000..6b9c974 --- /dev/null +++ b/src/actions/standard-bsky-actions/ReskeetActions.ts @@ -0,0 +1,65 @@ +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { AbstractAction } from '../AbstractAction'; + +export class CreateReskeetAction extends AbstractAction { + constructor( + protected skeetUri: + | string + | ((arg0: HandlerAgent, ...args: any) => string), + protected skeetCid: + | string + | ((arg0: HandlerAgent, ...args: any) => string) + ) { + super(); + } + + static make( + skeetUri: string | ((arg0: HandlerAgent, ...args: any) => string), + skeetCid: string | ((arg0: HandlerAgent, ...args: any) => string) + ): CreateReskeetAction { + return new CreateReskeetAction(skeetUri, skeetCid); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { + const uri: string = AbstractAction.getStringOrFunctionReturn( + this.skeetUri, + handlerAgent, + ...args + ); + const cid: string = AbstractAction.getStringOrFunctionReturn( + this.skeetCid, + handlerAgent, + ...args + ); + + await handlerAgent.reskeetSkeet(uri, cid); + } +} + +export class DeleteReskeetAction extends AbstractAction { + constructor( + protected skeetUri: + | string + | ((arg0: HandlerAgent, ...args: any) => string) + ) { + super(); + } + + static make( + skeetUri: string | ((arg0: HandlerAgent, ...args: any) => string) + ): DeleteReskeetAction { + return new DeleteReskeetAction(skeetUri); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { + const uri: string = AbstractAction.getStringOrFunctionReturn( + this.skeetUri, + handlerAgent, + ...args + ); + + await handlerAgent.unreskeetSkeet(uri); + } +} diff --git a/src/actions/standard-bsky-actions/SkeetActions.ts b/src/actions/standard-bsky-actions/SkeetActions.ts new file mode 100644 index 0000000..c9c279d --- /dev/null +++ b/src/actions/standard-bsky-actions/SkeetActions.ts @@ -0,0 +1,73 @@ +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { AbstractAction } from '../AbstractAction'; +import { JetstreamReply } from '../../types/JetstreamTypes'; + +export class CreateSkeetAction extends AbstractAction { + constructor( + protected skeetText: + | string + | ((arg0: HandlerAgent, ...args: any) => string), + protected skeetReply: + | JetstreamReply + | ((arg0: HandlerAgent, ...args: any) => JetstreamReply) + | undefined = undefined + ) { + super(); + } + + static make( + skeetText: string | ((arg0: HandlerAgent, ...args: any) => string), + skeetReply: + | JetstreamReply + | ((arg0: HandlerAgent, ...args: any) => JetstreamReply) + | undefined = undefined + ): CreateSkeetAction { + return new CreateSkeetAction(skeetText, skeetReply); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { + const text: string = AbstractAction.getStringOrFunctionReturn( + this.skeetText, + handlerAgent, + ...args + ); + let reply = undefined; + if (this.skeetReply !== undefined) { + if (typeof this.skeetReply == 'function') { + reply = this.skeetReply(handlerAgent, ...args); + } else { + reply = this.skeetReply; + } + } + + await handlerAgent.createSkeet(text, reply); + } +} + +export class DeleteSkeetAction extends AbstractAction { + constructor( + protected skeetUri: + | string + | ((arg0: HandlerAgent, ...args: any) => string) + ) { + super(); + } + + static make( + skeetUri: string | ((arg0: HandlerAgent, ...args: any) => string) + ): DeleteSkeetAction { + return new DeleteSkeetAction(skeetUri); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { + const uri: string = AbstractAction.getStringOrFunctionReturn( + this.skeetUri, + handlerAgent, + ...args + ); + + await handlerAgent.deleteSkeet(uri); + } +} diff --git a/src/agent/HandlerAgent.ts b/src/agent/HandlerAgent.ts index 92e4dc7..c828979 100644 --- a/src/agent/HandlerAgent.ts +++ b/src/agent/HandlerAgent.ts @@ -6,13 +6,14 @@ import { RichText, } from '@atproto/api'; import { debugLog } from '../utils/logging-utils'; -import { ProfileView } from '@atproto/api/dist/client/types/app/bsky/actor/defs'; import { - CreateSkeetMessage, - JetstreamMessage, - Reply, - Subject, + JetstreamEventCommit, + JetstreamReply, + JetstreamSubject, + NewSkeetRecord, } from '../types/JetstreamTypes'; +import { DebugLog } from '../utils/DebugLog'; +import fs from 'node:fs'; export class HandlerAgent { private did: string | undefined; @@ -59,7 +60,19 @@ export class HandlerAgent { if (!this.agent) { this.agent = this.initializeBskyAgent(); } - if (this.agent) { + + if (fs.existsSync(this.getSessionLocation())) { + DebugLog.warn('AGENT', 'Existing session. Loading session'); + + const loadedSession: AtpSessionData | undefined = + await this.loadSessionData(); + if (loadedSession) { + this.setSession = loadedSession; + } + } + + if (this.agent && this.session === undefined) { + DebugLog.warn('AGENT', 'No existing session. creating session'); await this.agent.login({ identifier: this.handle, password: this.password, @@ -70,8 +83,10 @@ export class HandlerAgent { ); } else { debugLog('AGENT', `${this.agentName} is authenticated!`); - // console.log() } + } + + if (this.session !== undefined) { await this.agent.resumeSession(this.session); if (!this.agent) { @@ -81,75 +96,152 @@ export class HandlerAgent { } } + getSessionLocation(): string { + const path = process.env?.SESSION_DATA_PATH ?? '.'; + return `${path}/${this.agentName}-session.json`; + } + + async saveSessionData(session: AtpSessionData): Promise { + const sessionLocation = this.getSessionLocation(); + return new Promise((resolve, reject) => { + fs.writeFile(sessionLocation, JSON.stringify(session), (err) => { + if (err) { + DebugLog.error( + 'AGENT', + `Failed to save session data. ${err.message}` + ); + reject(new Error('Failed to save')); + } else { + resolve(); + } + }); + }); + } + + async loadSessionData(): Promise { + const sessionLocation = this.getSessionLocation(); + return new Promise((resolve, reject) => { + fs.readFile(sessionLocation, 'utf8', (err, data) => { + if (err) { + DebugLog.error( + 'AGENT', + `Failed to read session data. ${err.message}` + ); + resolve(undefined); + } else { + try { + resolve(JSON.parse(data) as AtpSessionData); + } catch (parseError) { + DebugLog.error( + 'AGENT', + `Failed to parse session data. ${parseError}` + ); + resolve(undefined); + } + } + }); + }); + } //endregion //region Follower Interactions + + /** + * getProfile + */ + + async getProfile(did: string) { + const response = await this.agent?.getProfile({ actor: did }); + return response?.data; + } + /** * */ - async getFollows(userDID: string | undefined = undefined) { + async getFollows( + userDID: string | undefined = undefined, + cursor: string | undefined = undefined, + limit: number = 50 + ) { if (userDID === undefined) { userDID = this.getDid; } - const resp = await this.agent?.getFollows({ actor: userDID }); - return resp?.data.follows; + const body = { + actor: userDID, + cursor: cursor, + limit: limit, + }; + const resp = await this.agent?.getFollows(body); + return resp?.data; } /** * */ - async getFollowers(userDID: string | undefined = undefined) { + async getFollowers( + userDID: string | undefined = undefined, + cursor: string | undefined = undefined, + limit: number = 50 + ) { if (userDID === undefined) { userDID = this.getDid; } - const resp = await this.agent?.getFollowers({ actor: userDID }); - return resp?.data.followers; + const body = { + actor: userDID, + cursor: cursor, + limit: limit, + }; + const resp = await this.agent?.getFollowers(body); + return resp?.data; } /** * */ async isFollowing(userDID: string): Promise { - const getFollowsResponse = await this.getFollows(); - - if (Array.isArray(getFollowsResponse)) { - const following = this.extractDIDsFromProfiles(getFollowsResponse); - return following.includes(userDID); + const followProfile = await this.getProfile(userDID); + if (!followProfile) { + return false; } - return false; + const viewer = followProfile?.viewer; + if (!viewer?.following) { + return false; + } + return true; } /** * */ async isFollowedBy(userDID: string): Promise { - const getFollowerResponse = await this.getFollowers(); - if (Array.isArray(getFollowerResponse)) { - const followers = this.extractDIDsFromProfiles(getFollowerResponse); - return followers.includes(userDID); + const followProfile = await this.getProfile(userDID); + if (!followProfile) { + return false; } - return false; + const viewer = followProfile?.viewer; + if (!viewer?.followedBy) { + return false; + } + return true; } /** * */ - async followUser(did: string): Promise { - await this.agent?.follow(did); + async followUser(userDID: string): Promise { + await this.agent?.follow(userDID); return true; } /** * */ - async unfollowUser(did: string): Promise { - const getFollowsResponse = await this.getFollows(); - - if (!Array.isArray(getFollowsResponse)) { + async unfollowUser(userDID: string): Promise { + const followProfile = await this.getProfile(userDID); + if (!followProfile) { return false; } - const resp = this.getRecordForDid(did, getFollowsResponse); - const followLink = resp?.viewer?.following; + const followLink = followProfile?.viewer?.following; if (followLink) { await this.agent?.deleteFollow(followLink); return true; @@ -161,20 +253,21 @@ export class HandlerAgent { //region Follow Helpers - /** - * - * @param follows - */ - extractDIDsFromProfiles(follows: ProfileView[]): string[] { - return follows.map((item) => item.did); - } - - getRecordForDid( - targetDid: string, - data: ProfileView[] - ): ProfileView | undefined { - return data.find((item) => item.did === targetDid); - } + // + // /** + // * + // * @param follows + // */ + // extractDIDsFromProfiles(follows: ProfileView[]): string[] { + // return follows.map((item) => item.did); + // } + // + // getRecordForDid( + // targetDid: string, + // data: ProfileView[] + // ): ProfileView | undefined { + // return data.find((item) => item.did === targetDid); + // } //endregion @@ -196,24 +289,29 @@ export class HandlerAgent { */ async createSkeet( newPostDetails: string, - skeetReply: Reply | undefined = undefined + skeetReply: JetstreamReply | undefined = undefined ) { // TODO Add in handling for facets and maybe images? const replyText = new RichText({ text: newPostDetails, }); - if (skeetReply == undefined) { - // if it's not a reply - return await this.agent?.post({ - text: replyText.text, - }); - } else { - return await this.agent?.post({ - // @ts-ignore - reply: skeetReply, - text: replyText.text, - }); + if (this.getAgent !== undefined) { + await replyText.detectFacets(this.getAgent); + } + // @ts-ignore + const record: NewSkeetRecord = { + text: replyText.text, + }; + if (skeetReply !== undefined) { + // @ts-ignore + record.reply = skeetReply; } + if (replyText.facets !== undefined) { + // @ts-ignore + record.facets = replyText.facets; + } + + return await this.agent?.post(record as any); } /** @@ -237,9 +335,12 @@ export class HandlerAgent { /** * */ - async unlikeSkeet(likeURI: string) { - await this.agent?.deleteLike(likeURI); - // TODO error handling + async unlikeSkeet(skeetUri: string) { + const likeUri: string = await this.findLikeRecord(skeetUri); + if (likeUri == '') { + return false; + } + await this.agent?.deleteLike(likeUri); return true; } @@ -248,22 +349,133 @@ export class HandlerAgent { */ async reskeetSkeet(skeetURI: string, skeetCID: string) { await this.agent?.repost(skeetURI, skeetCID); - // TODO add error handling return true; } /** * */ - async unreskeetSkeet(reskeetURI: string) { - await this.agent?.deleteRepost(reskeetURI); - // TODO error handling + async unreskeetSkeet(skeetUri: string) { + const reskeetUri: string = await this.findRepostRecord(skeetUri); + if (reskeetUri == '') { + return false; + } + await this.agent?.deleteRepost(reskeetUri); return true; } //endregion //region Post Helpers + + /** + * Finds a record that is similar to a given skeet URI. + * + * @param {string} skeetUri - The skeet URI to find a similar record for. + * @param {string} [cursor=undefined] - The optional cursor to paginate results. + * @param {number} [attempt=1] - The number of attempts made to find the record. + * @returns {Promise} A promise that resolves to the ID of the found record. + */ + async findLikeRecord( + skeetUri: string, + cursor: string | undefined = undefined, + attempt: number = 1 + ): Promise { + return this.findSpecificRecord( + 'app.bsky.feed.like', + 'like', + skeetUri, + cursor, + attempt + ); + } + + /** + * Finds a repost record based on the given parameters. + * @param {string} skeetUri - The skeet URI to search for. + * @param {string | undefined} cursor - Optional cursor for pagination. + * @param {number} attempt - The attempt number for the search. + * @return {Promise} - A Promise that resolves to the found repost record. + */ + async findRepostRecord( + skeetUri: string, + cursor: string | undefined = undefined, + attempt: number = 1 + ): Promise { + return this.findSpecificRecord( + 'app.bsky.feed.repost', + 'repost', + skeetUri, + cursor, + attempt + ); + } + + /** + * Finds a specific record in a collection. + * + * @param {string} collectionType - The type of collection to search in. + * @param {string} errorName - The name of the error associated with the record. + * @param {string} skeetUri - The URI of the record to find. + * @param {string[]} cursor - The cursor used for pagination (optional). + * @param {number} attempt - The attempt number (optional). + * + * @return {Promise} - A promise that resolves to the URI of the found record. + */ + async findSpecificRecord( + collectionType: string, + errorName: string, + skeetUri: string, + cursor: string | undefined = undefined, + attempt: number = 1 + ): Promise { + const params = { + repo: this.getDid, + collection: collectionType, + limit: 100, + }; + + if (cursor !== undefined) { + // @ts-ignore + params['cursor'] = cursor; + } + const recordsResponse = + await this.agent?.api.com.atproto.repo.listRecords(params, {}); + if (recordsResponse == undefined) { + DebugLog.error( + 'Handler Agent', + `Failed to retrieve ${errorName} records` + ); + return ''; + } + const records = recordsResponse.data.records; + cursor = recordsResponse.data.cursor; + const record = records.find( + // @ts-ignore + (record) => record.value.subject.uri === skeetUri + ); + + if (record == null) { + DebugLog.info('Handler Agent', `Attempt ${attempt} to find record`); + if (attempt >= 25) { + DebugLog.error( + 'Handler Agent', + `Failed to retrieve ${errorName} record` + ); + return ''; + } + return await this.findSpecificRecord( + collectionType, + errorName, + skeetUri, + cursor, + attempt + 1 + ); + } + + return record.uri; + } + /** * */ @@ -274,30 +486,43 @@ export class HandlerAgent { /** * */ - postedByAgent(message: JetstreamMessage) { - return message.did === this.getDid; //TODO Test + postedByAgent(message: JetstreamEventCommit) { + return message.did === this.getDid; } /** * */ - generateURIFromCreateMessage(message: CreateSkeetMessage) { - return `at://${message.did}/app.bsky.feed.post/${message.rkey}`; + generateURIFromCreateMessage(message: JetstreamEventCommit) { + return `at://${message.did}/app.bsky.feed.post/${message.commit.rkey}`; } /** * */ - generateReplyFromMessage(message: CreateSkeetMessage): Reply { - let reply: Reply; //TODO Test - const parentReply: Subject = { - cid: message.cid, - uri: `at://${message.did}/app.bsky.feed.post/${message.rkey}`, + generateReplyFromMessage(event: JetstreamEventCommit): JetstreamReply { + let reply: JetstreamReply; + if (typeof event.commit.record?.subject == 'string') { + return { + root: { + uri: '', + cid: '', + }, + parent: { + uri: '', + cid: '', + }, + }; + } + const parentReply: JetstreamSubject = { + // @ts-ignore + cid: event.commit.cid, + uri: `at://${event.did}/app.bsky.feed.post/${event.commit.rkey}`, }; - // if message is a reply - if (message.record.reply) { + // if event is a reply + if (event.commit.record?.reply) { reply = { - root: message.record.reply.root, + root: event.commit.record.reply.root, parent: parentReply, }; } else { @@ -309,12 +534,61 @@ export class HandlerAgent { return reply; } - hasPostReply(message: CreateSkeetMessage) { - return 'reply' in message.record && message.record?.reply !== undefined; + hasPostReply(message: JetstreamEventCommit) { + if (!message.commit.record) return false; + + return ( + 'reply' in message.commit.record && + message.commit?.record?.reply !== undefined + ); + } + + getPostReply(message: JetstreamEventCommit) { + return message?.commit?.record?.reply; + } + + async getPostLikeCount(postUri: string): Promise { + return await this.getPostCount(postUri, 'like'); + } + + async getPostRepostCount(postUri: string): Promise { + return await this.getPostCount(postUri, 'repost'); } - getPostReply(message: CreateSkeetMessage) { - return message.record.reply; + async getPostReplyCount(postUri: string): Promise { + return await this.getPostCount(postUri, 'reply'); + } + + async getPostQuoteCount(postUri: string): Promise { + return await this.getPostCount(postUri, 'quote'); + } + + async getPostCount( + postUri: string, + countType: 'like' | 'repost' | 'reply' | 'quote' + ): Promise { + const resp = await this.agent?.getPostThread({ + uri: postUri, + }); + if (!resp) return -1; + + const post = resp.data.thread.post; + + // Using a switch statement to retrieve the appropriate count based on the input parameter + switch (countType) { + case 'like': + // @ts-ignore + return post.likeCount; + case 'repost': + // @ts-ignore + return post.repostCount; + case 'reply': + // @ts-ignore + return post.replyCount; + case 'quote': + // @ts-ignore + return post.quoteCount; + } } //endregion @@ -410,6 +684,10 @@ export class HandlerAgent { */ public set setSession(sess: AtpSessionData | undefined) { this.session = sess; + if (this.session !== undefined) { + DebugLog.warn('AGENT', 'Saving session'); + this.saveSessionData(this.session); + } } /** @@ -418,7 +696,7 @@ export class HandlerAgent { */ public get getSession(): AtpSessionData | boolean { if (!this.session) { - return false; //TODO Test + return false; } return this.session; } diff --git a/src/agent/README.md b/src/agent/README.md index ef425cd..fe0d34c 100644 --- a/src/agent/README.md +++ b/src/agent/README.md @@ -1,19 +1,20 @@ # Handler Agent -This is the class/object that our bluesky agent will be created and acted upon from. -It contains a ton of functions to interact with bluesky, and a number of helper functions for interacting with Jetstream Messages +This is the class/object that our Bluesky agent will be created and acted upon from. +It contains numerous functions to interact with Bluesky, along with helper functions for interacting with Jetstream Messages. -- Class Properties: private variables like did, session, agent, agentName, handle, and password. -- Class Constructor: Initializes the HandlerAgent class. It set ups the BskyAgent and session details. -- Methods for Initialization and Authentication: `initializeBskyAgent` and `authenticate` set up and authenticate the agent respectively. -- Methods for Follower Interactions: `getFollows`, `getFollowers`, `isFollowing`, `isFollowedBy`, `followUser`, and `unfollowUser` help in managing follower relationships. -- Methods for Posting Interactions: `post`, `createSkeet`, `deleteSkeet`, `likeSkeet`, `unlikeSkeet`, `reskeetSkeet`, `unreskeetSkeet` are used for creating, deleting, liking and reposting skeets. -- Helper Functions: Functions like `getDIDFromUri`, `generateReplyFromMessage`, `extractDIDsFromProfiles`, `getRecordForDid`, as well as others related to post helpers and getters and setters to fetch or manipulate properties of objects. -- Getters and Setters: Getter and setter methods for manipulating the private variables. +Session data is stored locally for each individual handlerAgent, change the location with the env variable `SESSION_DATA_PATH` -## Usage +### Class Properties +- `agentName`: The name of the agent. +- `handle`: The handle used for authentication. +- `password`: The password used for authentication. +- `did`: (private) Decentralized Identifier for the agent, assigned after authentication. +- `session`: (private) The current session data. +- `agent`: (private) Instance of `BskyAgent`. -To initialize the Handler agent, pass in a name, the handle, and the App password you'll use to authenticate with the Bsky service +### Class Constructor +Initializes the `HandlerAgent` class, setting up the `BskyAgent` and session details. ```typescript const myBotHandlerAgent = new HandlerAgent( @@ -21,6 +22,61 @@ const myBotHandlerAgent = new HandlerAgent( 'Handle.bsky.social', 'AppPassword' ); + +await myBotHandlerAgent.authenticate() ``` -# More docs coming soon (maybe, idk, writing docs is hard, especially for a 400 line clas) +### Methods for Initialization and Authentication +- `initializeBskyAgent()`: Initializes the `BskyAgent` with the required service URL and session persistence. +- `authenticate()`: Authenticates the agent using the provided handle and password. +- `getSessionLocation()`: Returns the filepath of the session.json that is/will be stored when an agent authenticates. Can be modified by setting SESSION_DATA_PATH to the directory you want the session data saved in (i.e. SESSION_DATA_LOCATION='./agentData') +- `saveSessionData(session: AtpSessionData)`: Saves the agent session data to the file at the path from `getSessionLocation` +- `loadSessionData(): AtpSessionData|undefined`: Loads the session data stored in the json file at the path from `getSessionLocation` + +### Methods for Follower Interactions +- `getProfile(did: string)`: Retrieves the profile of the user with the specified DID. +- `getFollows(userDID: string | undefined, cursor: string | undefined, limit: number)`: Retrieves the list of users followed by the specified user. +- `getFollowers(userDID: string | undefined, cursor: string | undefined, limit: number)`: Retrieves the list of followers of the specified user. +- `isFollowing(userDID: string)`: Checks if the current agent is following the specified user. +- `isFollowedBy(userDID: string)`: Checks if the current agent is followed by the specified user. +- `followUser(userDID: string)`: Follows the specified user. +- `unfollowUser(userDID: string)`: Unfollows the specified user. + +### Methods for Posting Interactions +- `post(details: Partial)`: Creates a post with the given details. +- `createSkeet(newPostDetails: string, skeetReply: JetstreamReply | undefined)`: Creates a skeet (post) with the specified details and optional reply. +- `deleteSkeet(skeetURI: string)`: Deletes the skeet with the given URI. +- `likeSkeet(skeetURI: string, skeetCID: string)`: Likes the skeet with the given URI and CID. +- `unlikeSkeet(skeetURI: string)`: Unlikes the skeet with the given URI. +- `reskeetSkeet(skeetURI: string, skeetCID: string)`: Reskeets (reposts) the skeet with the given URI. +- `unreskeetSkeet(skeetURI: string)`: Unreskeets (deletes repost) for the skeet with the given URI. +- `getPostLikeCount(skeetURI: string)` : Returns the number of likes on a post +- `getPostRepostCount(skeetURI: string)` : Returns the number of reposts on a post +- `getPostReplyCount(skeetURI: string)` : Returns the number of replies on a post +- `getPostQuoteCount(skeetURI: string)` : Returns the number of quotes on a post +- `getPostCount(skeetURI: string, countType: 'like' | 'repost' | 'reply' | 'quote')` : Returns the number of {something} on a post + +### Helper Functions +- `findLikeRecord(skeetURI: string, cursor: string | undefined, attempt: number)`: Finds a record similar to the specified skeet URI. +- `findRepostRecord(skeetURI: string, cursor: string | undefined, attempt: number)`: Finds a repost record for the specified skeet URI. +- `findSpecificRecord(collectionType: string, errorName: string, skeetURI: string, cursor: string | undefined, attempt: number)`: Finds a specific record in the specified collection for the given skeet URI. +- `getDIDFromUri(uri: string)`: Extracts the DID from a URI. +- `postedByAgent(message: JetstreamEventCommit)`: Checks if a message was posted by the agent. +- `generateURIFromCreateMessage(message: JetstreamEventCommit)`: Generates a URI from a `JetstreamEventCommit`. +- `generateReplyFromMessage(message: JetstreamEventCommit)`: Generates a reply from a `JetstreamEventCommit`. +- `hasPostReply(message: JetstreamEventCommit)`: Checks if a message has a reply. +- `getPostReply(message: JetstreamEventCommit)`: Retrieves the reply from a message. + +### Getters and Setters +- `setAgent`: Sets the agent. +- `getAgent`: Retrieves the agent. +- `setAgentName`: Sets the agent name. +- `getAgentName`: Retrieves the agent name. +- `setHandle`: Sets the handle. +- `getHandle`: Retrieves the handle. +- `setPassword`: Sets the password. +- `getPassword`: Retrieves the password. +- `setDid`: Sets the DID. +- `getDid`: Retrieves the DID. +- `setSession`: Sets the session. +- `getSession`: Retrieves the session. \ No newline at end of file diff --git a/src/firehose/JetstreamSubscription.ts b/src/firehose/JetstreamSubscription.ts deleted file mode 100644 index c859fd7..0000000 --- a/src/firehose/JetstreamSubscription.ts +++ /dev/null @@ -1,159 +0,0 @@ -import WebSocket from 'ws'; -import { DebugLog } from '../utils/DebugLog'; -import { - CreateMessage, - CreateSkeetMessage, - DeleteMessage, -} from '../types/JetstreamTypes'; -import { CreateSkeetHandler } from '../handlers/skeet/CreateSkeetHandler'; -import { MessageHandler } from '../handlers/AbstractMessageHandler'; - -export interface CreateAndDeleteHandlersInterface { - c?: MessageHandler[]; - d?: MessageHandler[]; -} -export interface JetstreamSubscriptionHandlers { - post?: CreateAndDeleteHandlersInterface; - like?: CreateAndDeleteHandlersInterface; - repost?: CreateAndDeleteHandlersInterface; - follow?: CreateAndDeleteHandlersInterface; -} - -export class JetstreamSubscription { - //@ts-ignore - private wsClient: WebSocket; - public lastMessageTime: number | undefined; - - /** - * Creates a new instance of the Firehose Subscription. - * - * @param {JetstreamSubscriptionHandlers} handlerControllers - An array of handler controllers. - * @param {string} wsURL - The WebSocket URL to connect to. Defaults to `wss://bsky.network`. - */ - constructor( - private handlerControllers: JetstreamSubscriptionHandlers, - private wsURL: string = 'ws://localhost:6008/subscribe' - ) { - this.generateWsURL(); - DebugLog.info('FIREHOSE', `Websocket URL: ${this.wsURL}`); - } - - public set setWsURL(url: string) { - this.wsURL = url; - } - - generateWsURL() { - const properties = ['post', 'like', 'repost', 'follow']; - const queryParams: string[] = properties - // @ts-ignore - .filter((property) => Boolean(this.handlerControllers[property])) - .map((property) => { - const prefix = property === 'follow' ? 'graph' : 'feed'; - return `wantedCollections=app.bsky.${prefix}.${property}`; - }); - this.setWsURL = `${this.wsURL}?${queryParams.join('&')}`; - } - - /** - * - */ - public createSubscription() { - DebugLog.warn('FIREHOSE', `Initializing`); - - this.wsClient = new WebSocket(this.wsURL); - - this.wsClient.on('open', () => { - DebugLog.info('FIREHOSE', `Connection Opened`); - }); - - this.wsClient.on('message', (data, isBinary) => { - const message = !isBinary ? data : data.toString(); - if (typeof message === 'string') { - const data = JSON.parse(message); - switch (data.opType) { - case 'c': - this.handleCreate(data as CreateMessage); - break; - case 'd': - this.handleDelete(data as DeleteMessage); - break; - } - } - }); - - this.wsClient.on('close', () => { - DebugLog.error('JETSTREAM', 'Subscription Closed'); - this.wsClient.close(); - setTimeout(() => { - this.createSubscription(); - }, 5000); - }); - } - - // TODO There has got to be a better way to do this, I'm just to high to do it now - handleCreate(createMessage: CreateMessage) { - switch (createMessage.collection) { - case 'app.bsky.feed.post': - this.handlerControllers.post?.c?.forEach( - (handler: CreateSkeetHandler) => { - handler.handle(createMessage as CreateSkeetMessage); - } - ); - break; - case 'app.bsky.feed.like': - this.handlerControllers.like?.c?.forEach( - (handler: MessageHandler) => { - handler.handle(createMessage); - } - ); - break; - case 'app.bsky.feed.repost': - this.handlerControllers.repost?.c?.forEach( - (handler: MessageHandler) => { - handler.handle(createMessage); - } - ); - break; - case 'app.bsky.graph.follow': - this.handlerControllers.follow?.c?.forEach( - (handler: MessageHandler) => { - handler.handle(createMessage); - } - ); - break; - } - } - - handleDelete(deleteMessage: DeleteMessage) { - switch (deleteMessage.collection) { - case 'app.bsky.feed.post': - this.handlerControllers.post?.d?.forEach( - (handler: MessageHandler) => { - handler.handle(deleteMessage); - } - ); - break; - case 'app.bsky.feed.like': - this.handlerControllers.like?.d?.forEach( - (handler: MessageHandler) => { - handler.handle(deleteMessage); - } - ); - break; - case 'app.bsky.feed.repost': - this.handlerControllers.repost?.d?.forEach( - (handler: MessageHandler) => { - handler.handle(deleteMessage); - } - ); - break; - case 'app.bsky.graph.follow': - this.handlerControllers.follow?.d?.forEach( - (handler: MessageHandler) => { - handler.handle(deleteMessage); - } - ); - break; - } - } -} diff --git a/src/handlers/AbstractHandler.ts b/src/handlers/AbstractHandler.ts new file mode 100644 index 0000000..8dd0876 --- /dev/null +++ b/src/handlers/AbstractHandler.ts @@ -0,0 +1,53 @@ +import { AbstractValidator } from '../validations/AbstractValidator'; +import { HandlerAgent } from '../agent/HandlerAgent'; +import { AbstractAction } from '../actions/AbstractAction'; +import { DebugLog } from '../utils/DebugLog'; + +export class AbstractHandler { + protected HANDLER_NAME: string = 'Abstract Handler'; + + constructor( + protected validators: Array, + protected actions: Array, + public handlerAgent: HandlerAgent + ) {} + + static make(...args: any): AbstractHandler { + throw new Error('Method Not Implemented! Use constructor.'); + } + + async shouldTrigger(...args: any): Promise { + const willTrigger = true; + for (const validator of this.validators) { + const response = await validator.shouldTrigger( + this.handlerAgent, + ...args + ); + if (!response) { + return false; + } + } + return willTrigger; + } + + async runActions(...args: any) { + for (const action of this.actions) { + await action.handle(this.handlerAgent, ...args); + } + } + + //@ts-ignore + async handle( + handlerAgent: HandlerAgent | undefined, + ...args: any + ): Promise { + const shouldTrigger = await this.shouldTrigger(...args); + if (shouldTrigger) { + try { + await this.runActions(...args); + } catch (exception) { + DebugLog.error(this.HANDLER_NAME, exception as string); + } + } + } +} diff --git a/src/handlers/AbstractMessageHandler.ts b/src/handlers/AbstractMessageHandler.ts deleted file mode 100644 index 3af7bdb..0000000 --- a/src/handlers/AbstractMessageHandler.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { AbstractValidator } from '../validations/AbstractValidator'; -import { HandlerAgent } from '../agent/HandlerAgent'; -import { CreateSkeetMessage, JetstreamMessage } from '../types/JetstreamTypes'; -import { AbstractMessageAction } from '../actions/AbstractMessageAction'; -import { DebugLog } from '../utils/DebugLog'; - -export abstract class AbstractMessageHandler { - constructor( - private validators: Array, - private actions: Array, - public handlerAgent: HandlerAgent - ) {} - - static make(...args: any): AbstractMessageHandler { - throw new Error('Method Not Implemented! Use constructor.'); - } - - async shouldTrigger(message: JetstreamMessage): Promise { - const willTrigger = true; - for (const validator of this.validators) { - const response = await validator.shouldTrigger( - message, - this.handlerAgent - ); - if (!response) { - return false; - } - } - return willTrigger; - } - - async runActions(message: JetstreamMessage) { - for (const action of this.actions) { - await action.handle(message, this.handlerAgent); - } - } - - //@ts-ignore - abstract async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent | null - ): Promise; -} - -export class MessageHandler extends AbstractMessageHandler { - constructor( - validators: Array, - actions: Array, - handlerAgent: HandlerAgent - ) { - super(validators, actions, handlerAgent); - return this; - } - - static make( - validators: Array, - actions: Array, - handlerAgent: HandlerAgent - ): MessageHandler { - return new MessageHandler(validators, actions, handlerAgent); - } - - async handle(message: JetstreamMessage): Promise { - const shouldTrigger = await this.shouldTrigger(message); - if (shouldTrigger) { - try { - await this.runActions(message); - } catch (exception) { - DebugLog.error('Message Handler', exception as string); - } - } - } -} diff --git a/src/handlers/TestHandler.ts b/src/handlers/TestHandler.ts index 687042a..1faca0c 100644 --- a/src/handlers/TestHandler.ts +++ b/src/handlers/TestHandler.ts @@ -1,28 +1,28 @@ import { AbstractValidator } from '../validations/AbstractValidator'; -import { AbstractMessageAction } from '../actions/AbstractMessageAction'; import { HandlerAgent } from '../agent/HandlerAgent'; -import { JetstreamMessage } from '../types/JetstreamTypes'; import { DebugLog } from '../utils/DebugLog'; -import { - AbstractMessageHandler, - MessageHandler, -} from './AbstractMessageHandler'; +import { MessageHandler } from './message-handlers/MessageHandler'; +import { AbstractHandler } from './AbstractHandler'; +import { AbstractAction } from '../actions/AbstractAction'; -export class TestHandler extends AbstractMessageHandler { +export class TestHandler extends AbstractHandler { constructor( validators: Array, - actions: Array, + actions: Array, handlerAgent: HandlerAgent ) { super(validators, actions, handlerAgent); return this; } - async handle(message: JetstreamMessage): Promise { - const shouldTrigger = await this.shouldTrigger(message); + async handle( + handlerAgent: HandlerAgent | undefined, + ...args: any + ): Promise { + const shouldTrigger = await this.shouldTrigger(...args); if (shouldTrigger) { try { - await this.runActions(message); + await this.runActions(...args); } catch (exception) { DebugLog.error('Message Handler', exception as string); } diff --git a/src/handlers/message-handlers/MessageHandler.ts b/src/handlers/message-handlers/MessageHandler.ts new file mode 100644 index 0000000..d04ae22 --- /dev/null +++ b/src/handlers/message-handlers/MessageHandler.ts @@ -0,0 +1,51 @@ +import { AbstractValidator } from '../../validations/AbstractValidator'; +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { AbstractMessageAction } from '../../actions/message-actions/AbstractMessageAction'; +import { AbstractHandler } from '../AbstractHandler'; +import { + JetstreamEventCommit, + JetstreamReply, +} from '../../types/JetstreamTypes'; + +// @ts-ignore +export class MessageHandler extends AbstractHandler { + protected HANDLER_NAME: string = 'Message Handler'; + + constructor( + protected validators: Array, + protected actions: Array, + public handlerAgent: HandlerAgent + ) { + super(validators, actions, handlerAgent); + return this; + } + + static getUriFromMessage( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): string { + return handlerAgent.generateURIFromCreateMessage(message); + } + + static getCidFromMessage( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): string { + return message.commit.cid; + } + + static generateReplyFromMessage( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): JetstreamReply { + return handlerAgent.generateReplyFromMessage(message); + } + + static make( + validators: Array, + actions: Array, + handlerAgent: HandlerAgent + ): MessageHandler { + return new MessageHandler(validators, actions, handlerAgent); + } +} diff --git a/src/handlers/README.md b/src/handlers/message-handlers/README.md similarity index 100% rename from src/handlers/README.md rename to src/handlers/message-handlers/README.md diff --git a/src/handlers/message-handlers/TestMessageHandler.ts b/src/handlers/message-handlers/TestMessageHandler.ts new file mode 100644 index 0000000..07e2f44 --- /dev/null +++ b/src/handlers/message-handlers/TestMessageHandler.ts @@ -0,0 +1,34 @@ +import { MessageHandler } from './MessageHandler'; +import { AbstractValidator } from '../../validations/AbstractValidator'; +import { AbstractMessageAction } from '../../actions/message-actions/AbstractMessageAction'; +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { + JetstreamEventCommit, + JetstreamMessage, +} from '../../types/JetstreamTypes'; +import { DebugLog } from '../../utils/DebugLog'; + +export class TestMessageHandler extends MessageHandler { + constructor( + validators: Array, + actions: Array, + handlerAgent: HandlerAgent + ) { + super(validators, actions, handlerAgent); + return this; + } + + async handle( + handlerAgent: HandlerAgent | undefined, + message: JetstreamEventCommit + ): Promise { + const shouldTrigger = await this.shouldTrigger(message); + if (shouldTrigger) { + try { + await this.runActions(message); + } catch (exception) { + DebugLog.error('Message Handler', exception as string); + } + } + } +} diff --git a/src/handlers/message-handlers/premade-handlers/BadBotHandler.ts b/src/handlers/message-handlers/premade-handlers/BadBotHandler.ts new file mode 100644 index 0000000..325dcd7 --- /dev/null +++ b/src/handlers/message-handlers/premade-handlers/BadBotHandler.ts @@ -0,0 +1,49 @@ +import { IsBadBotValidator } from '../../../validations/message-validators/BotValidators'; +import { DebugLogAction } from '../../../actions/LoggingActions'; +import { HandlerAgent } from '../../../agent/HandlerAgent'; +import { JetstreamEventCommit } from '../../../types/JetstreamTypes'; +import { MessageHandler } from '../MessageHandler'; +import { CreateLikeAction } from '../../../actions/standard-bsky-actions/LikeActions'; +import { CreateSkeetAction } from '../../../actions/standard-bsky-actions/SkeetActions'; + +// TODO I want to have .make() available on the premade handlers, but +// the parameters don't match with the CreateSkeetHandler .make(), so i need a ts-ignore +// I don't like using ts-ignore, cause that can lead to bugs popping up +// @ts-ignore +export class BadBotHandler extends MessageHandler { + constructor( + public handlerAgent: HandlerAgent, + public response: string = "I'm sorry 😓" + ) { + super( + [IsBadBotValidator.make()], + [ + CreateSkeetAction.make( + response, + MessageHandler.generateReplyFromMessage + ), + CreateLikeAction.make( + MessageHandler.getUriFromMessage, + MessageHandler.getCidFromMessage + ), + DebugLogAction.make('BAD BOT', `Told I'm bad :(`), + ], + handlerAgent + ); + } + + static make( + handlerAgent: HandlerAgent, + response: string | undefined = undefined + ): BadBotHandler { + return new BadBotHandler(handlerAgent, response); + } + + // TODO Update to use JetstreamEventCommit + async handle( + handlerAgent: HandlerAgent | undefined, + message: JetstreamEventCommit + ): Promise { + return super.handle(this.handlerAgent, message); + } +} diff --git a/src/handlers/message-handlers/premade-handlers/GoodBotHandler.ts b/src/handlers/message-handlers/premade-handlers/GoodBotHandler.ts new file mode 100644 index 0000000..dcebaa1 --- /dev/null +++ b/src/handlers/message-handlers/premade-handlers/GoodBotHandler.ts @@ -0,0 +1,47 @@ +import { IsGoodBotValidator } from '../../../validations/message-validators/BotValidators'; +import { HandlerAgent } from '../../../agent/HandlerAgent'; +import { JetstreamEventCommit } from '../../../types/JetstreamTypes'; +// import { CreateSkeetHandler } from '../skeet/CreateSkeetHandler'; +import { MessageHandler } from '../MessageHandler'; +import { CreateLikeAction } from '../../../actions/standard-bsky-actions/LikeActions'; +import { DebugLogAction } from '../../../actions/LoggingActions'; +import { CreateSkeetAction } from '../../../actions/standard-bsky-actions/SkeetActions'; + +// TODO see comment at top of BadBotHandler +// @ts-ignore +export class GoodBotHandler extends MessageHandler { + constructor( + public handlerAgent: HandlerAgent, + public response: string = 'Thank you đŸĨš' + ) { + super( + [IsGoodBotValidator.make()], + [ + CreateSkeetAction.make( + response, + MessageHandler.generateReplyFromMessage + ), + CreateLikeAction.make( + MessageHandler.getUriFromMessage, + MessageHandler.getCidFromMessage + ), + DebugLogAction.make('GOOD BOT', `Told I'm good :)`), + ], + handlerAgent + ); + } + + static make( + handlerAgent: HandlerAgent, + response: string | undefined = undefined + ): GoodBotHandler { + return new GoodBotHandler(handlerAgent, response); + } + + async handle( + handlerAgent: HandlerAgent | undefined, + message: JetstreamEventCommit + ): Promise { + return super.handle(this.handlerAgent, message); + } +} diff --git a/src/handlers/message-handlers/premade-handlers/OfflineHandler.ts b/src/handlers/message-handlers/premade-handlers/OfflineHandler.ts new file mode 100644 index 0000000..eeaa870 --- /dev/null +++ b/src/handlers/message-handlers/premade-handlers/OfflineHandler.ts @@ -0,0 +1,36 @@ +import { InputIsCommandValidator } from '../../../validations/message-validators/post/StringValidators'; +import { HandlerAgent } from '../../../agent/HandlerAgent'; +import { ReplyToSkeetAction } from '../../../actions/message-actions/post/SkeetMessageActions'; +import { JetstreamEventCommit } from '../../../types/JetstreamTypes'; +import { MessageHandler } from '../MessageHandler'; + +// @ts-ignore +export class OfflineHandler extends MessageHandler { + constructor( + public handlerAgent: HandlerAgent, + private command: string, + private response: string = 'Bot functionality offline' + ) { + super( + [new InputIsCommandValidator(command, false)], + [new ReplyToSkeetAction(response)], + handlerAgent + ); + } + + static make( + handlerAgent: HandlerAgent, + command: string, + response: string | undefined = undefined + ): OfflineHandler { + return new OfflineHandler(handlerAgent, command, response); + } + + // TODO Update to use JetstreamEventCommit + async handle( + handlerAgent: HandlerAgent | undefined, + message: JetstreamEventCommit + ): Promise { + return super.handle(this.handlerAgent, message); + } +} diff --git a/src/handlers/premade-handlers/README.md b/src/handlers/message-handlers/premade-handlers/README.md similarity index 92% rename from src/handlers/premade-handlers/README.md rename to src/handlers/message-handlers/premade-handlers/README.md index 0015c66..295e042 100644 --- a/src/handlers/premade-handlers/README.md +++ b/src/handlers/message-handlers/premade-handlers/README.md @@ -44,8 +44,8 @@ export class ExampleHandler extends CreateSkeetHandler { ); } - async handle(message: CreateSkeetMessage): Promise { - return super.handle(message); + async handle(handlerAgent: HandlerAgent | undefined, message: CreateSkeetMessage): Promise { + return super.handle(this.handlerAgent, message); } } ``` diff --git a/src/handlers/premade-handlers/BadBotHandler.ts b/src/handlers/premade-handlers/BadBotHandler.ts deleted file mode 100644 index 56fb467..0000000 --- a/src/handlers/premade-handlers/BadBotHandler.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { IsBadBotValidator } from '../../validations/BotValidators'; -import { DebugLogAction } from '../../actions/LoggingActions'; -import { HandlerAgent } from '../../agent/HandlerAgent'; -import { CreateSkeetHandler } from '../skeet/CreateSkeetHandler'; -import { ReplyToSkeetAction } from '../../actions/post/SkeetActions'; -import { CreateSkeetMessage } from '../../types/JetstreamTypes'; - -// TODO I want to have .make() available on the premade handlers, but -// the parameters don't match with the CreateSkeetHandler .make(), so i need a ts-ignore -// I don't like using ts-ignore, cause that can lead to bugs popping up -// @ts-ignore -export class BadBotHandler extends CreateSkeetHandler { - constructor( - public handlerAgent: HandlerAgent, - public response: string = "I'm sorry 😓" - ) { - super( - [new IsBadBotValidator()], - [ - new ReplyToSkeetAction(response), - new DebugLogAction('BAD BOT', `Told I'm bad :(`), - ], - handlerAgent - ); - } - - static make( - handlerAgent: HandlerAgent, - response: string | undefined = undefined - ): BadBotHandler { - return new BadBotHandler(handlerAgent, response); - } - - async handle(message: CreateSkeetMessage): Promise { - return super.handle(message); - } -} diff --git a/src/handlers/premade-handlers/GoodBotHandler.ts b/src/handlers/premade-handlers/GoodBotHandler.ts deleted file mode 100644 index ace34a6..0000000 --- a/src/handlers/premade-handlers/GoodBotHandler.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { IsGoodBotValidator } from '../../validations/BotValidators'; -import { DebugLogAction } from '../../actions/LoggingActions'; -import { HandlerAgent } from '../../agent/HandlerAgent'; -import { ReplyToSkeetAction } from '../../actions/post/SkeetActions'; -import { CreateSkeetMessage } from '../../types/JetstreamTypes'; -import { CreateSkeetHandler } from '../skeet/CreateSkeetHandler'; - -// TODO see comment at top of BadBotHandler -// @ts-ignore -export class GoodBotHandler extends CreateSkeetHandler { - constructor( - public handlerAgent: HandlerAgent, - public response: string = 'Thank you đŸĨš' - ) { - super( - [new IsGoodBotValidator()], - [ - new ReplyToSkeetAction(response), - new DebugLogAction('GOOD BOT', `Told I'm good :)`), - ], - handlerAgent - ); - } - - static make( - handlerAgent: HandlerAgent, - response: string | undefined = undefined - ): GoodBotHandler { - return new GoodBotHandler(handlerAgent, response); - } - - async handle(message: CreateSkeetMessage): Promise { - return super.handle(message); - } -} diff --git a/src/handlers/premade-handlers/OfflineHandler.ts b/src/handlers/premade-handlers/OfflineHandler.ts deleted file mode 100644 index 042e261..0000000 --- a/src/handlers/premade-handlers/OfflineHandler.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { InputIsCommandValidator } from '../../validations/post/StringValidators'; -import { HandlerAgent } from '../../agent/HandlerAgent'; -import { CreateSkeetHandler } from '../skeet/CreateSkeetHandler'; -import { ReplyToSkeetAction } from '../../actions/post/SkeetActions'; -import { CreateSkeetMessage } from '../../types/JetstreamTypes'; - -// @ts-ignore -export class OfflineHandler extends CreateSkeetHandler { - constructor( - public handlerAgent: HandlerAgent, - private command: string, - private response: string = 'Bot functionality offline' - ) { - super( - [new InputIsCommandValidator(command, false)], - [new ReplyToSkeetAction(response)], - handlerAgent - ); - } - - static make( - handlerAgent: HandlerAgent, - command: string, - response: string | undefined = undefined - ): OfflineHandler { - return new OfflineHandler(handlerAgent, command, response); - } - - async handle(message: CreateSkeetMessage): Promise { - return super.handle(message); - } -} diff --git a/src/handlers/skeet/CreateSkeetHandler.ts b/src/handlers/skeet/CreateSkeetHandler.ts deleted file mode 100644 index 212823a..0000000 --- a/src/handlers/skeet/CreateSkeetHandler.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { AbstractMessageHandler } from '../AbstractMessageHandler'; -import { AbstractMessageAction } from '../../actions/AbstractMessageAction'; -import { - CreateSkeetMessage, - JetstreamMessage, -} from '../../types/JetstreamTypes'; -import { AbstractValidator } from '../../validations/AbstractValidator'; -import { HandlerAgent } from '../../agent/HandlerAgent'; -import { DebugLog } from '../../utils/DebugLog'; - -// @ts-ignore -export class CreateSkeetHandler extends AbstractMessageHandler { - constructor( - validators: Array, - actions: Array, - handlerAgent: HandlerAgent - ) { - super(validators, actions, handlerAgent); - return this; - } - - static make( - validators: Array, - actions: Array, - handlerAgent: HandlerAgent - ): CreateSkeetHandler { - return new CreateSkeetHandler(validators, actions, handlerAgent); - } - - async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent | null = null - ): Promise { - const shouldTrigger = await this.shouldTrigger( - message as CreateSkeetMessage - ); - if (shouldTrigger) { - try { - await this.runActions(message as CreateSkeetMessage); - } catch (exception) { - DebugLog.error('Skeet Handler', exception as string); - } - } - } -} diff --git a/src/index.ts b/src/index.ts index c99713d..ded3005 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,43 +1,57 @@ /** * Handlers */ -export * from './handlers/AbstractMessageHandler'; -export { CreateSkeetHandler } from './handlers/skeet/CreateSkeetHandler'; +export { AbstractHandler } from './handlers/AbstractHandler'; +export { MessageHandler } from './handlers/message-handlers/MessageHandler'; export { TestHandler } from './handlers/TestHandler'; +export { TestMessageHandler } from './handlers/message-handlers/TestMessageHandler'; /** * Premade Handlers */ -export { GoodBotHandler } from './handlers/premade-handlers/GoodBotHandler'; -export { BadBotHandler } from './handlers/premade-handlers/BadBotHandler'; -export { OfflineHandler } from './handlers/premade-handlers/OfflineHandler'; +export { GoodBotHandler } from './handlers/message-handlers/premade-handlers/GoodBotHandler'; +export { BadBotHandler } from './handlers/message-handlers/premade-handlers/BadBotHandler'; +export { OfflineHandler } from './handlers/message-handlers/premade-handlers/OfflineHandler'; /** * Validators */ -export { AbstractValidator } from './validations/AbstractValidator'; +export * from './validations/AbstractValidator'; export * from './validations/TestValidator'; export * from './validations/LogicalValidators'; -export * from './validations/BotValidators'; -export * from './validations/GenericValidators'; +export * from './validations/interval-validators/IsSpecifiedTimeValidator'; +export * from './validations/interval-validators/IsFourTwentyValidator'; +export * from './validations/message-validators/BotValidators'; +export * from './validations/message-validators/GenericValidators'; -export * from './validations/post/StringValidators'; -export * from './validations/post/PostValidators'; -export * from './validations/follow/FollowValidators'; +export * from './validations/message-validators/post/StringValidators'; +export * from './validations/message-validators/post/PostValidators'; +export * from './validations/message-validators/follow/FollowValidators'; +export * from './validations/message-validators/repost/RepostValidators'; +export * from './validations/message-validators/like/LikeUserValidators'; +export * from './validations/message-validators/like/LikeCountValidators'; /** * Actions */ -export * from './actions/AbstractMessageAction'; -export * from './actions/TestAction'; +export * from './actions/AbstractAction'; export * from './actions/FunctionAction'; export * from './actions/LoggingActions'; -export * from './actions/post/SkeetActions'; +export * from './actions/standard-bsky-actions/SkeetActions'; +export * from './actions/standard-bsky-actions/LikeActions'; +export * from './actions/standard-bsky-actions/ReskeetActions'; +export * from './actions/standard-bsky-actions/FollowActions'; +export * from './actions/message-actions/AbstractMessageAction'; +export * from './actions/message-actions/TestAction'; +export * from './actions/message-actions/MessageLoggingActions'; +export * from './actions/message-actions/post/SkeetMessageActions'; /** - * Firehose + * Subscriptions */ -export * from './firehose/JetstreamSubscription'; +export * from './subscriptions/AbstractSubscription'; +export * from './subscriptions/IntervalSubscription'; +export * from './subscriptions/firehose/JetstreamSubscription'; /** * Agent diff --git a/src/subscriptions/AbstractSubscription.ts b/src/subscriptions/AbstractSubscription.ts new file mode 100644 index 0000000..08a9e45 --- /dev/null +++ b/src/subscriptions/AbstractSubscription.ts @@ -0,0 +1,22 @@ +import { AbstractHandler } from '../handlers/AbstractHandler'; +import { JetstreamSubscriptionHandlers } from './firehose/JetstreamSubscription'; +import { IntervalSubscriptionHandlers } from './IntervalSubscription'; + +export abstract class AbstractSubscription { + constructor( + protected handlers: + | JetstreamSubscriptionHandlers + | IntervalSubscriptionHandlers + | AbstractHandler[] + ) {} + + abstract createSubscription(): AbstractSubscription; + + abstract stopSubscription(): AbstractSubscription; + + restartSubscription(): AbstractSubscription { + this.stopSubscription(); + this.createSubscription(); + return this; + } +} diff --git a/src/subscriptions/IntervalSubscription.ts b/src/subscriptions/IntervalSubscription.ts new file mode 100644 index 0000000..101a2c6 --- /dev/null +++ b/src/subscriptions/IntervalSubscription.ts @@ -0,0 +1,42 @@ +import { AbstractSubscription } from './AbstractSubscription'; +import { AbstractHandler } from '../handlers/AbstractHandler'; + +export interface IntervalSchemaInterface { + intervalSeconds: number; // seconds + handlers: AbstractHandler[]; +} + +export type IntervalSubscriptionHandlers = IntervalSchemaInterface[]; + +export class IntervalSubscription extends AbstractSubscription { + protected _intervals: any[] = []; + + constructor(protected intervalHandlers: IntervalSubscriptionHandlers) { + super(intervalHandlers); + } + + get intervals() { + return this._intervals; + } + + createSubscription(): IntervalSubscription { + this.intervalHandlers.forEach((intervalCollection) => { + this._intervals.push( + setInterval(async () => { + for (const handler of intervalCollection.handlers) { + await handler.handle(undefined); + } + }, intervalCollection.intervalSeconds * 1000) + ); + }); + return this; + } + + stopSubscription(): IntervalSubscription { + this.intervals.forEach((interval) => { + clearInterval(interval); + }); + this._intervals = []; + return this; + } +} diff --git a/src/subscriptions/README.md b/src/subscriptions/README.md new file mode 100644 index 0000000..535aebe --- /dev/null +++ b/src/subscriptions/README.md @@ -0,0 +1,60 @@ +# Subscriptions + +### Jetstream subscription +See [firehose README](./firehose/README.md) + +### Interval subscription + +The interval subscription works a lot like the Jetstream subscription, where it has handlers with validators and actions, the difference being, the validators and actions don't get a Jetstream event. +To start using an interval subscription, you'll need your intervalSubscriptionHandlers array. In this example, I'm using the IsIt420 bot as an example. + +IntervalSubscriptionHandlers is an array of IntervalSchemaInterface. +The IntervalSchemaInterface consists of two parts. the `intervalSeconds`, which is how often the interval will run, and the `handlers` which are multiple abstract handlers with validators, actions, and a HandlerAgent. + +```typescript +const intervalSubscriptionHandlers: IntervalSubscriptionHandlers = [ + { + intervalSeconds: 60, + handlers:[ + new AbstractHandler( + [IsFourTwentyValidator.make()], + [ + LogInputTextAction.make("Is 4:20"), + CreateSkeetAction.make("It's 4:20 somewhere!") + ], + testAgent), + new AbstractHandler( + [IsFourTwentyValidator.make().not()], + [ + LogInputTextAction.make("Is not 4:20"), + CreateSkeetAction.make("It's not 4:20 anywhere :(") + + ], + testAgent) + ] + } +] +``` + +Once the handlers are prepared, create the subscription object, and start the intervals +```typescript +const intervalSubscription = new IntervalSubscription( + intervalSubscriptionHandlers +) + +intervalSubscription.createSubscription() +``` + +Ensure that you have also created and authenticated your Handler Agent + +```typescript +// before the Handlers are made +const testAgent = new HandlerAgent( + 'test-bot', + Bun.env.IS_IT_FOUR_TWENTY_HANDLE, + Bun.env.IS_IT_FOUR_TWENTY_PASSWORD +); + +// before the subscription starts +await testAgent.authenticate() +``` diff --git a/src/subscriptions/firehose/JetstreamSubscription.ts b/src/subscriptions/firehose/JetstreamSubscription.ts new file mode 100644 index 0000000..a60ed48 --- /dev/null +++ b/src/subscriptions/firehose/JetstreamSubscription.ts @@ -0,0 +1,204 @@ +import WebSocket from 'ws'; +import { DebugLog } from '../../utils/DebugLog'; +import { + JetstreamEvent, + JetstreamEventCommit, +} from '../../types/JetstreamTypes'; +import { MessageHandler } from '../../handlers/message-handlers/MessageHandler'; +import { AbstractSubscription } from '../AbstractSubscription'; + +export interface CreateAndDeleteHandlersInterface { + c?: MessageHandler[]; + d?: MessageHandler[]; +} + +export interface JetstreamSubscriptionHandlers { + post?: CreateAndDeleteHandlersInterface; + like?: CreateAndDeleteHandlersInterface; + repost?: CreateAndDeleteHandlersInterface; + follow?: CreateAndDeleteHandlersInterface; +} + +export class JetstreamSubscription extends AbstractSubscription { + //@ts-ignore + public wsClient: WebSocket; + public lastMessageTime: number | undefined; + public restart: boolean = false; + public restartDelay: number = 5; // seconds + + /** + * Creates a new instance of the Firehose Subscription. + * + * @param {JetstreamSubscriptionHandlers} handlerControllers - An array of handler controllers. + * @param {string} wsURL - The WebSocket URL to connect to. Defaults to `wss://bsky.network`. + */ + constructor( + protected handlerControllers: JetstreamSubscriptionHandlers, + protected wsURL: string = 'ws://localhost:6008/subscribe' + ) { + super(handlerControllers); + this.generateWsURL(); + DebugLog.info('FIREHOSE', `Websocket URL: ${this.wsURL}`); + } + + public set setWsURL(url: string) { + this.wsURL = url; + } + + generateWsURL() { + const properties = ['post', 'like', 'repost', 'follow']; + const queryParams: string[] = properties + // @ts-ignore + .filter((property) => Boolean(this.handlerControllers[property])) + .map((property) => { + const prefix = property === 'follow' ? 'graph' : 'feed'; + return `wantedCollections=app.bsky.${prefix}.${property}`; + }); + if (queryParams.length > 0) { + this.setWsURL = `${this.wsURL}?${queryParams.join('&')}`; + } + } + + /** + * + */ + public createSubscription(): this { + DebugLog.warn('FIREHOSE', `Initializing`); + + this.wsClient = new WebSocket(this.wsURL); + + this.wsClient.on('open', this.handleOpen); + + this.wsClient.on( + 'message', + (data: WebSocket.RawData, isBinary: boolean) => { + const message = data.toString(); + // console.log(isBinary); + if (typeof message === 'string') { + const event: JetstreamEvent = JSON.parse(message); + if (event.kind === 'commit') { + switch (event.commit?.operation) { + case 'create': + this.handleCreate( + event as JetstreamEventCommit + ); + break; + case 'delete': + this.handleDelete( + event as JetstreamEventCommit + ); + break; + } + } + } + } + ); + + this.wsClient.on('close', () => { + DebugLog.error('JETSTREAM', 'Subscription Closed'); + this.restart = true; + this.wsClient?.close(); + if (this.restart) { + DebugLog.warn( + 'JETSTREAM', + 'Subscription restarting in 5 seconds' + ); + setTimeout(() => { + this.createSubscription(); + this.restart = false; + }, this.restartDelay * 1000); + } + }); + + this.wsClient.on('error', (err) => { + this.handleError(err); + }); + + return this; + } + + public handleOpen() { + DebugLog.info('FIREHOSE', `Connection Opened`); + } + + public handleError(err: Error) { + console.log(err); + DebugLog.error('FIREHOSE', `Error: ${err.message}`); + this.restart = true; + } + + public stopSubscription(restart: boolean = false): this { + this.wsClient.close(); + this.restart = restart; + return this; + } + + // TODO There has got to be a better way to do this, I'm just to high to do it now + handleCreate(createEvent: JetstreamEventCommit) { + switch (createEvent.commit.collection) { + case 'app.bsky.feed.post': + this.handlerControllers.post?.c?.forEach( + // @ts-ignore + (handler: MessageHandler) => { + // TODO Update MessageHandler for new types + handler.handle(undefined, createEvent); + } + ); + break; + case 'app.bsky.feed.like': + this.handlerControllers.like?.c?.forEach( + (handler: MessageHandler) => { + handler.handle(undefined, createEvent); + } + ); + break; + case 'app.bsky.feed.repost': + this.handlerControllers.repost?.c?.forEach( + (handler: MessageHandler) => { + handler.handle(undefined, createEvent); + } + ); + break; + case 'app.bsky.graph.follow': + this.handlerControllers.follow?.c?.forEach( + (handler: MessageHandler) => { + handler.handle(undefined, createEvent); + } + ); + break; + } + } + + handleDelete(deleteEvent: JetstreamEventCommit) { + switch (deleteEvent.commit.collection) { + case 'app.bsky.feed.post': + this.handlerControllers.post?.d?.forEach( + (handler: MessageHandler) => { + handler.handle(undefined, deleteEvent); + } + ); + break; + case 'app.bsky.feed.like': + this.handlerControllers.like?.d?.forEach( + (handler: MessageHandler) => { + handler.handle(undefined, deleteEvent); + } + ); + break; + case 'app.bsky.feed.repost': + this.handlerControllers.repost?.d?.forEach( + (handler: MessageHandler) => { + handler.handle(undefined, deleteEvent); + } + ); + break; + case 'app.bsky.graph.follow': + this.handlerControllers.follow?.d?.forEach( + (handler: MessageHandler) => { + handler.handle(undefined, deleteEvent); + } + ); + break; + } + } +} diff --git a/src/firehose/README.md b/src/subscriptions/firehose/README.md similarity index 100% rename from src/firehose/README.md rename to src/subscriptions/firehose/README.md diff --git a/src/types/JetstreamTypes.ts b/src/types/JetstreamTypes.ts index 1df808e..e5fc1b6 100644 --- a/src/types/JetstreamTypes.ts +++ b/src/types/JetstreamTypes.ts @@ -1,89 +1,142 @@ -export interface AspectRatio { +export interface JetstreamAspectRatio { height: number; width: number; } -export interface Ref { +export interface JetstreamRef { $link: string; } -export interface Subject { +export interface JetstreamSubject { cid: string; uri: string; } -export interface Image { +export interface JetstreamImage { $type: string; - ref: Ref; + ref: JetstreamRef; mimeType: string; size: number; } -export interface External { +export interface JetstreamExternal { description: string; - thumb: Image; + thumb: JetstreamImage; title: string; uri: string; } -export interface Feature { +export interface JetstreamFeature { $type: string; uri: string; } -export interface Index { +export interface JetstreamIndex { byteEnd: number; byteStart: number; } -export interface Facet { - features: Feature[]; - index: Index; +export interface JetstreamFacet { + features: JetstreamFeature[]; + index: JetstreamIndex; } -export interface ImageEmbed { +export interface JetstreamImageEmbed { alt: string; - aspectRatio: AspectRatio; - image: Image; + aspectRatio: JetstreamAspectRatio; + image: JetstreamImage; } -export interface Reply { - parent: Subject; - root: Subject; +export interface JetstreamReply { + parent: JetstreamSubject; + root: JetstreamSubject; } -export interface Record { - $type: CollectionType; +export type JetstreamCollectionType = + | 'app.bsky.feed.post' + | 'app.bsky.feed.like' + | 'app.bsky.feed.repost' + | 'app.bsky.graph.follow'; + +export interface JetstreamEvent { + did: string; + time_us: number; + kind: 'commit' | 'account' | 'identity'; + commit?: JetstreamCommit; + identity?: JetstreamIdentity; + account?: JetstreamAccount; +} + +export interface JetstreamCommit { + rev: string; + operation: 'create' | 'delete' | 'update'; + collection: JetstreamCollectionType; + rkey: string; + record?: JetstreamRecord | NewSkeetRecord; + cid: string; +} + +export interface JetstreamEventCommit extends JetstreamEvent { + commit: JetstreamCommit; +} + +export interface JetstreamIdentity { + did: string; + handle: string; + seq: number; + time: string; +} + +export interface JetstreamEventIdentity extends JetstreamEvent { + identity: JetstreamIdentity; +} + +export interface JetstreamAccount { + active: boolean; + did: string; + seq: number; + time: string; +} + +export interface JetstreamEventAccount extends JetstreamEvent { + account: JetstreamAccount; +} + +export interface JetstreamRecord { + $type: JetstreamCollectionType; createdAt: string; - subject?: Subject | string; + subject?: JetstreamSubject | string; + reply?: JetstreamReply; } -export interface CreateSkeetRecord extends Record { - embed?: { $type: string; images?: ImageEmbed[]; external?: External }; - facets?: Facet[]; +export interface NewSkeetRecord { + $type: JetstreamCollectionType; + createdAt: string; + embed?: { + $type: string; + images?: JetstreamImageEmbed[]; + external?: JetstreamExternal; + }; + facets?: JetstreamFacet[]; langs?: string[]; - text?: string; - reply?: Reply; + text: string; + reply?: JetstreamReply; + subject?: JetstreamSubject | string; } - -export type CollectionType = - | 'app.bsky.feed.post' - | 'app.bsky.feed.like' - | 'app.bsky.feed.repost' - | 'app.bsky.graph.follow'; +// Deprecated export type OperationType = 'c' | 'd'; export interface JetstreamMessage { did: string; seq: number; opType: OperationType; - collection: CollectionType | string; + collection: JetstreamCollectionType | string; rkey: string; cid: string; } export interface CreateMessage extends JetstreamMessage { - record: Record; + record: JetstreamRecord; } export interface DeleteMessage extends JetstreamMessage {} @@ -91,3 +144,15 @@ export interface DeleteMessage extends JetstreamMessage {} export interface CreateSkeetMessage extends JetstreamMessage { record: CreateSkeetRecord; } + +export interface CreateSkeetRecord extends JetstreamRecord { + embed?: { + $type: string; + images?: JetstreamImageEmbed[]; + external?: JetstreamExternal; + }; + facets?: JetstreamFacet[]; + langs?: string[]; + text?: string; + reply?: JetstreamReply; +} diff --git a/src/types/README.md b/src/types/README.md index fd74f20..01608b5 100644 --- a/src/types/README.md +++ b/src/types/README.md @@ -2,150 +2,195 @@ ## Jetstream Interfaces -### JetstreamMessage +### JetstreamEvent -A standard message format that's used as the base for other message types. +A standard message format that's used as the base for other message types. Received from the jetstream firehose -| Property | Type | Description | -| ---------- | ------ | ------------------------------------------------- | -| did | string | Identifier for the Distributed IDentifiers (DIDs) | -| seq | number | Sequence number of the message | -| opType | string | Operation type of the message (c or d) | -| collection | string | Specific collection that the message belongs to | -| rkey | string | Routing key for the message | +| Property | Type | Description | +|-----------|-------------------|---------------------------------| +| did | string | DID that took the action | +| time_us | number | When bluesky received the event | +| kind | string | 'commit', 'account', 'identity' | +| commit? | JetstreamCommit | Commit data | +| identity? | JetstreamIdentity | Identity data | +| account? | JetstreamAccount | Account data | -### CreateMessage extends JetstreamMessage +### JetstreamEventCommit extends JetstreamEvent -A message with attached record content aimed at creating a new record. +An event with the commit property, kind = 'commit' -| Property | Type | Description | -| -------- | ------ | ------------------------------------- | -| record | Record | The record information to be created. | +| Property | Type | Description | +|----------|-----------------|--------------------------------| +| commit | JetstreamCommit | The commit data from the event | + +### JetstreamCommit + +The commit data in a jetstream event + +| Property | Type | Description | +|------------|---------------------------------|--------------------------------------------| +| rev | string | rev | +| operation | string | 'create', 'delete', 'update' | +| collection | JetstreamCollectionType | Collection that the commit is operating on | +| reky | string | rkey of the commit item | +| cid | string | Cid of the commit item | +| record? | JetstreamRecord, NewSkeetRecord | Record Data | -### DeleteMessage extends JetstreamMessage +### JetstreamEventIdentity extends JetstreamEvent -A message intended to delete a certain record. +An event with the identity property, kind = 'identity' -### CreateSkeetMessage extends CreateMessage +| Property | Type | Description | +|----------|-------------------|----------------------------------| +| identity | JetstreamIdentity | The identity data from the event | + +### JetstreamIdentity + +The identity data in a jetstream event + +| Property | Type | Description | +|----------|--------|------------------------| +| did | string | did of the identity | +| handle | string | handle of the identity | +| seq | number | sequence number | +| time | string | time of event | -A special type of message used for record creation with the Skeet type. +### JetstreamEventAccount extends JetstreamEvent -| Property | Type | Description | -| -------- | ----------------- | ------------------------------------ | -| record | CreateSkeetRecord | The Skeet record data to be created. | +An event with the account property, kind = 'account' -### AspectRatio +| Property | Type | Description | +|----------|------------------|---------------------------------| +| account | JetstreamAccount | The account data from the event | + +### JetstreamAccount + +The account data in a jetstream event + +| Property | Type | Description | +|----------|---------|--------------------------------------| +| did | string | did of the identity | +| active | boolean | whether the account is active or not | +| seq | number | sequence number | +| time | string | time of event | + +### JetstreamAspectRatio Defines the ratio of width to height for an element in numeric terms | Property | Type | Description | -| -------- | ------ | ---------------------- | +|----------|--------|------------------------| | width | number | Width of the element. | | height | number | Height of the element. | -### Ref +### JetstreamRef A reference object that points to a certain link. | Property | Type | Description | -| -------- | ------ | ----------------------- | +|----------|--------|-------------------------| | $link | string | The URL reference link. | -### Subject +### JetstreamSubject Represents a unique subject entity in the system. | Property | Type | Description | -| -------- | ------ | ------------------------------------- | +|----------|--------|---------------------------------------| | cid | string | The unique identifier of the subject. | | uri | string | The URI of the subject. | -### Image +### JetstreamImage Represents an image resource. -| Property | Type | Description | -| -------- | ------ | ----------------------------------------- | -| $type | string | Specifies the type of the object. | -| ref | Ref | A reference to where the image is stored. | -| mimeType | string | The MIME type of the image. | -| size | number | The size of the image file. | +| Property | Type | Description | +|----------|--------------|-------------------------------------------| +| $type | string | Specifies the type of the object. | +| ref | JetstreamRef | A reference to where the image is stored. | +| mimeType | string | The MIME type of the image. | +| size | number | The size of the image file. | -### External +### JetstreamExternal Describes an external resource with an optional thumbnail image. -| Property | Type | Description | -| ----------- | ------ | ------------------------------------------ | -| description | string | Description of the external resource. | -| thumb | Image | Thumbnail image for the external resource. | -| title | string | The title of the external resource. | -| uri | string | The external resource URI. | +| Property | Type | Description | +|-------------|----------------|--------------------------------------------| +| description | string | Description of the external resource. | +| thumb | JetstreamImage | Thumbnail image for the external resource. | +| title | string | The title of the external resource. | +| uri | string | The external resource URI. | -### Feature +### JetstreamFeature A certain feature, parameters are left generic for flexibility. -| Property | Type | Description | -| -------- | ------ | ---------------------------------- | -| $type | string | Specifies the type of the feature. | -| uri | string | Feature reference or source URI. | +| Property | Type | Description | +|----------|--------|-------------------------------------------| +| $type | string | Specifies the type of the feature. | +| uri | string | JetstreamFeature reference or source URI. | -### Index +### JetstreamIndex Represents a byte range. | Property | Type | Description | -| --------- | ------ | ------------------------ | +|-----------|--------|--------------------------| | byteStart | number | Start of the byte range. | | byteEnd | number | End of the byte range. | -### Facet +### JetstreamFacet Contains information on features and their indexes. -| Property | Type | Description | -| -------- | --------- | --------------------------------------- | -| features | Feature[] | Array of features. | -| index | Index | Index object representing a byte range. | +| Property | Type | Description | +|----------|--------------------|--------------------------------------------------| +| features | JetstreamFeature[] | Array of features. | +| index | JetstreamIndex | JetstreamIndex object representing a byte range. | -### ImageEmbed +### JetstreamImageEmbed Represents embedded image data. -| Property | Type | Description | -| ----------- | ----------- | ----------------------------- | -| alt | string | Alternate text for the image. | -| aspectRatio | AspectRatio | Aspect ratio of the image. | -| image | Image | The image object. | +| Property | Type | Description | +|-------------|----------------------|-------------------------------| +| alt | string | Alternate text for the image. | +| aspectRatio | JetstreamAspectRatio | Aspect ratio of the image. | +| image | JetstreamImage | The image object. | -### Reply +### JetstreamReply Defines a reply with references to its parent and root subjects. -| Property | Type | Description | -| -------- | ------- | ------------------------------------- | -| parent | Subject | Reference to the parent of the reply. | -| root | Subject | Reference to the root of the reply. | +| Property | Type | Description | +|----------|------------------|---------------------------------------| +| parent | JetstreamSubject | Reference to the parent of the reply. | +| root | JetstreamSubject | Reference to the root of the reply. | -### Record +### JetstreamRecord A generic record with timestamps and associated subject. -| Property | Type | Description | -| --------- | ---------------- | -------------------------------------------------------- | -| $type | string | The type of the record. | -| createdAt | string | The creation timestamp of the record. | -| subject | Subject / string | The associated subject of the record. Or DID as a string | +| Property | Type | Description | +|-----------|---------------------------|----------------------------------------------------------| +| $type | string | The type of the record. | +| createdAt | string | The creation timestamp of the record. | +| subject? | JetstreamSubject / string | The associated subject of the record. Or DID as a string | +| reply? | JetstreamReply / string | The associated reply of the record | + +### NewSkeetRecord -### CreateSkeetRecord extends Record +The Record object in an event commit when a skeet is created -A specific type of record intended to be created in the system. +| Property | Type | Description | +|-----------|---------------------------|---------------------| +| $type | JetstreamCollectionType | collection type | +| createdAt | string | time of creation | +| text | string | sequence number | +| embed? | object | embeded image data | +| facets? | JetstreamFacet[] | array of facet data | +| langs? | string[] | array of langs | +| subject? | JetstreamSubject / string | Subject of commit | +| reply? | JetstreamReply | reply of commit | -| Property | Type | Description | -| -------- | -------- | ------------------------------ | -| embed | object | Optional embedded object data. | -| facets | Facet[] | Optional array of facets. | -| langs | string[] | Optional array of languages. | -| text | string | Optional text data. | -| reply | Reply | Optional reply data. | diff --git a/src/types/examples.json b/src/types/examples.json new file mode 100644 index 0000000..190a30b --- /dev/null +++ b/src/types/examples.json @@ -0,0 +1,141 @@ +{ + "post": { + "create": { + "did": "did:plc:example", + "time_us": 1732557200926643, + "kind": "commit", + "commit": { + "rev": "3lbplfbxdxu2a", + "operation": "create", + "collection": "app.bsky.feed.post", + "rkey": "3lbplfbu5c52j", + "record": { + "$type": "app.bsky.feed.post", + "createdAt": "2024-11-24T17:56:39.159Z", + "langs": [ + "en" + ], + "reply": { + "parent": { + "cid": "bafyreig6j6qq7skwlcuqdhpxp7l3klib2t7m6yeipfcn64ekin55p4trp4", + "uri": "at://did:plc:example/app.bsky.feed.post/3lbpmigqbas24" + }, + "root": { + "cid": "bafyreic6az5oylokmhi2jk53hlkfunvlychsaz362pn76eh5licjut5xbi", + "uri": "at://did:plc:example/app.bsky.feed.post/3lbnc2kgt7s2n" + } + }, + "text": "Anytime lovey" + }, + "cid": "cidexample" + } + }, + "delete": { + "did": "did:plc:example", + "time_us": 1732557612617108, + "kind": "commit", + "commit": { + "rev": "3lbpmy7x4uc26", + "operation": "delete", + "collection": "app.bsky.feed.post", + "rkey": "3lbpgg3nsc22c" + } + } + }, + "follow": { + "create": { + "did": "did:plc:example", + "time_us": 1732557724767044, + "kind": "commit", + "commit": { + "rev": "3lbpng2oxco27", + "operation": "create", + "collection": "app.bsky.graph.follow", + "rkey": "3lbpng2ffm72u", + "record": { + "$type": "app.bsky.graph.follow", + "createdAt": "2024-11-24T18:32:52.373Z", + "subject": "did:plc:followed" + }, + "cid": "bafyreib4hkptkerknq5waovyzp5oi5rsfgchmce3mvyr4czowp7ebyzohy" + } + }, + "delete": { + "did": "did:plc:example", + "time_us": 1732557724786809, + "kind": "commit", + "commit": { + "rev": "3lbpng2sg222a", + "operation": "delete", + "collection": "app.bsky.graph.follow", + "rkey": "3lblwgkqieh2z" + } + } + }, + "repost": { + "create": { + "did": "did:plc:example", + "time_us": 1732557724781813, + "kind": "commit", + "commit": { + "rev": "3lbpng2quov25", + "operation": "create", + "collection": "app.bsky.feed.repost", + "rkey": "3lbpng2qlvn25", + "record": { + "$type": "app.bsky.feed.repost", + "createdAt": "2024-11-24T18:32:52.090Z", + "subject": { + "cid": "bafyreicz22chdj6cd27yrkx5qezwpdbwp7vrfvno3vt3jrq7hfttso4k6a", + "uri": "at://did:plc:example/app.bsky.feed.post/3lbpnvlgejs27" + } + }, + "cid": "bafyreie3il6a7wisf4bhajilqkwvqnmlcku26aqld336epdn7enqnmgfhi" + } + }, + "delete": { + "did": "did:plc:example", + "time_us": 1732557724784758, + "kind": "commit", + "commit": { + "rev": "3lbpng2rpek2f", + "operation": "delete", + "collection": "app.bsky.feed.repost", + "rkey": "3lbnkwjoej32g" + } + } + }, + "like": { + "create": { + "did": "did:plc:example", + "time_us": 1732557724767811, + "kind": "commit", + "commit": { + "rev": "3lbpng2qtxi2e", + "operation": "create", + "collection": "app.bsky.feed.like", + "rkey": "3lbpng2ql6a2e", + "record": { + "$type": "app.bsky.feed.like", + "createdAt": "2024-11-24T18:32:52.648Z", + "subject": { + "cid": "bafyreicz22chdj6cd27yrkx5qezwpdbwp7vrfvno3vt3jrq7hfttso4k6a", + "uri": "at://did:plc:example/app.bsky.feed.post/3lbpnvlgejs27" + } + }, + "cid": "bafyreief4vsnmgqr7ih2avhdbmwfqg4s7ltymikaj4wytm653j33vdosma" + } + }, + "delete": { + "did": "did:plc:example", + "time_us": 1732557724794024, + "kind": "commit", + "commit": { + "rev": "3lbpng2tjfu2u", + "operation": "delete", + "collection": "app.bsky.feed.like", + "rkey": "3l3berf6wkz2x" + } + } + } +} \ No newline at end of file diff --git a/src/types/factories/MessageFactories.ts b/src/types/factories/MessageFactories.ts index d4a2499..69874b1 100644 --- a/src/types/factories/MessageFactories.ts +++ b/src/types/factories/MessageFactories.ts @@ -1,257 +1,186 @@ import { - CollectionType, - CreateMessage, - CreateSkeetMessage, - CreateSkeetRecord, - JetstreamMessage, - Record, - Reply, + JetstreamAccount, + JetstreamCollectionType, + JetstreamCommit, + JetstreamEvent, + JetstreamIdentity, + JetstreamRecord, + NewSkeetRecord, } from '../JetstreamTypes'; import { AbstractTypeFactory } from './AbstractTypeFactory'; -import { CreateSkeetRecordFactory, ReplyFactory } from './RecordFactories'; +import { NewSkeetRecordFactory } from './RecordFactories'; -/** - * JetstreamMessageFactory - * - * A factory class for creating Jetstream messages. - */ -export class JetstreamMessageFactory extends AbstractTypeFactory { - public messageObject: JetstreamMessage; +export class JetstreamEventFactory extends AbstractTypeFactory { + public eventObject: JetstreamEvent; - /** - * Creates a new instance of the constructor. - * - * @constructor - */ constructor() { super(); - this.messageObject = { - cid: '', - collection: 'app.bsky.feed.post', - did: '', - opType: 'c', - rkey: '', - seq: 0, + this.eventObject = { + did: 'did:plc:example', + kind: 'commit', + time_us: 0, }; } - /** - * Returns an instance of JetstreamMessageFactory. - * - * @returns {JetstreamMessageFactory} An instance of JetstreamMessageFactory - */ - static factory(): JetstreamMessageFactory { - return new JetstreamMessageFactory(); + static factory(): JetstreamEventFactory { + return new JetstreamEventFactory(); } - static make(): JetstreamMessage { - return JetstreamMessageFactory.factory().create(); + static make(): JetstreamEvent { + return JetstreamEventFactory.factory().create(); } - - /** - * Returns the message object as a JetstreamMessage. - * - * @return {JetstreamMessage} The message object as a JetstreamMessage. - */ - create(): JetstreamMessage { - return this.messageObject; + create(): JetstreamEvent { + return this.eventObject as JetstreamEvent; } - /** - * Sets the collection type for the message object. - * - * @param {string} messageType - The type of collection to set. Valid values are "app.bsky.feed.post", "app.bsky.feed.like", "app.bsky.feed.repost", and "app.bsky.graph.follow". - * - * @return {JetstreamMessageFactory} - The modified collection object. - */ - collection(messageType: CollectionType) { - this.messageObject.collection = messageType; + fromDid(did: string) { + this.eventObject.did = did; return this; } - - /** - * Sets the operation type for the message object. - * - * @param {string} opType - The operation type. Must be either "c" or "d". - * @return {JetstreamMessageFactory} - Returns the updated instance of the class. - */ - opType(opType: 'c' | 'd') { - this.messageObject.opType = opType; + commit( + commit: JetstreamCommit | undefined = undefined + ): JetstreamEventFactory { + this.eventObject.kind = 'commit'; + if (commit === undefined) { + this.eventObject.commit = JetstreamCommitFactory.make(); + } else { + this.eventObject.commit = commit; + } return this; } +} - /** - * Sets the operation type to "c" (creation) for the message object. - * - * @return {JetstreamMessageFactory} - Returns the updated instance of the class. - */ - isCreation() { - this.messageObject.opType = 'c'; - return this; +export class JetstreamCommitFactory extends AbstractTypeFactory { + public eventObject: JetstreamCommit; + + constructor() { + super(); + this.eventObject = { + collection: 'app.bsky.feed.post', + operation: 'create', + rev: 'examplerev', + rkey: 'examplerkey', + cid: 'examplecid', + record: undefined, + }; + // get keys from commit, replace values in event object + } + + static factory(): JetstreamCommitFactory { + return new JetstreamCommitFactory(); + } + + static make(): JetstreamCommit { + return JetstreamCommitFactory.factory().create(); } - /** - * Sets the operation type to "d" (deletion) for the message object. - * - * @return {JetstreamMessageFactory} - Returns the updated instance of the class. - */ - isDeletion() { - this.messageObject.opType = 'd'; + create(): JetstreamCommit { + return this.eventObject as JetstreamCommit; + } + + rkey(rkey: string) { + this.eventObject.rkey = rkey; return this; } - /** - * Sets the 'did' property of the message object. - * - * @param {string} did - The value to set as the 'did' property. - * @return {JetstreamMessageFactory} - Returns the updated instance of the class. - */ - fromDid(did: string) { - this.messageObject.did = did; + record(record: JetstreamRecord | NewSkeetRecord) { + this.eventObject.record = record; return this; } - /** - * Set the value of the rkey property in the message object. - * - * @param {string} rkey - The rkey value to set. - * @return {JetstreamMessageFactory} - Returns the updated instance of the class. - */ - rkey(rkey: string) { - this.messageObject.rkey = rkey; + operation(operation: 'create' | 'update' | 'delete') { + this.eventObject.operation = operation; return this; } - /** - * Sets the CID for the message object. - * - * @param {string} cid - The custom identifier for the message. - * @return {JetstreamMessageFactory} - Returns the updated instance of the class. - */ - cid(cid: string) { - this.messageObject.cid = cid; + collection(collection: JetstreamCollectionType) { + this.eventObject.collection = collection; return this; } - /** - * Set the seq property of the message object. - * - * @param {number} seq - The value to set as the seq property. - * @return {JetstreamMessageFactory} - Returns the updated instance of the class. - */ - seq(seq: number) { - this.messageObject.seq = seq; + text(text: string) { + if (this.eventObject.record == undefined) { + this.eventObject.record = NewSkeetRecordFactory.factory() + .text(text) + .create(); + } else { + const tempRecord: NewSkeetRecord = this.eventObject + .record as NewSkeetRecord; + tempRecord.text = text; + this.eventObject.record = tempRecord; + } return this; } } -/** - * Represents a factory for creating create message objects. - */ -export class CreateMessageFactory extends JetstreamMessageFactory { - public messageObject: CreateMessage; +export class JetstreamIdentityFactory extends AbstractTypeFactory { + public eventObject: JetstreamIdentity; - /** - * Constructor for creating a new instance of the class. - * Initializes a message object with default values for its properties. - * - * @constructor - */ constructor() { super(); - this.messageObject = { - cid: '', - collection: 'app.bsky.feed.post', - did: '', - opType: 'c', - rkey: '', + this.eventObject = { + did: 'did:plc:example', + handle: 'handle.example', seq: 0, - record: { - $type: 'app.bsky.feed.post', - createdAt: '', - }, - } as CreateMessage; + time: '', + }; } - /** - * Creates a new instance of CreateMessageFactory. - * - * @return {CreateMessageFactory} The newly created instance of CreateMessageFactory. - */ - static factory(): CreateMessageFactory { - return new CreateMessageFactory(); + static factory(): JetstreamIdentityFactory { + return new JetstreamIdentityFactory(); } - static make(): CreateMessage { - return CreateMessageFactory.factory().create(); + static make(): JetstreamIdentity { + return JetstreamIdentityFactory.factory().create(); } - /** - * Creates a Jetstream message object. - * - * @returns {CreateMessage} The created CreateMessage object. - */ - create(): CreateMessage { - return this.messageObject as CreateMessage; + create(): JetstreamIdentity { + return this.eventObject as JetstreamIdentity; } - /** - * Sets the record for the message. - * - * @param {Record} record - The record to set for the message. - * @return {CreateMessageFactory} - The updated Factory with the new record. - */ - record(record: Record): CreateMessageFactory { - this.messageObject.record = record; + handle(handle: string): JetstreamIdentityFactory { + this.eventObject.handle = handle; + return this; + } + + sequence(seq: number): JetstreamIdentityFactory { + this.eventObject.seq = seq; return this; } } -// TODO what was I going to add here? Probably functions for setting text -// and adding embeds and stuff -export class CreateSkeetMessageFactory extends CreateMessageFactory { - public messageObject: CreateSkeetMessage; +export class JetstreamAccountFactory extends AbstractTypeFactory { + public eventObject: JetstreamAccount; constructor() { super(); - this.messageObject = { - cid: '', - collection: 'app.bsky.feed.post', - did: '', - opType: 'c', - rkey: '', + this.eventObject = { + active: true, + did: 'did:plc:example', seq: 0, - record: CreateSkeetRecordFactory.factory().create(), - } as CreateSkeetMessage; + time: Date.now().toString(), + }; } - static factory(): CreateSkeetMessageFactory { - return new CreateSkeetMessageFactory(); + static factory(): JetstreamAccountFactory { + return new JetstreamAccountFactory(); } - static make(): CreateSkeetMessage { - return CreateSkeetMessageFactory.factory().create(); - } - create(): CreateSkeetMessage { - return this.messageObject as CreateSkeetMessage; + static make(): JetstreamAccount { + return JetstreamAccountFactory.factory().create(); } - record(record: CreateSkeetRecord): CreateSkeetMessageFactory { - this.messageObject.record = record; - return this; + create(): JetstreamAccount { + return this.eventObject as JetstreamAccount; } - withReply(reply: Reply | undefined = undefined) { - if (reply === undefined) { - this.messageObject.record.reply = ReplyFactory.make(); - } else { - this.messageObject.record.reply = reply; - } + deactivate(): JetstreamAccountFactory { + this.eventObject.active = false; return this; } - withText(text: string) { - this.messageObject.record.text = text; + sequence(seq: number): JetstreamAccountFactory { + this.eventObject.seq = seq; return this; } } diff --git a/src/types/factories/RecordFactories.ts b/src/types/factories/RecordFactories.ts index f7a3a34..2719c83 100644 --- a/src/types/factories/RecordFactories.ts +++ b/src/types/factories/RecordFactories.ts @@ -1,14 +1,15 @@ import { CreateSkeetRecord, - Reply, - Subject, - Record, - CollectionType, + JetstreamCollectionType, + JetstreamRecord, + JetstreamReply, + JetstreamSubject, + NewSkeetRecord, } from '../JetstreamTypes'; import { AbstractTypeFactory } from './AbstractTypeFactory'; -export class RecordFactory extends AbstractTypeFactory { - public record: Record; +export class JetstreamRecordFactory extends AbstractTypeFactory { + public record: JetstreamRecord; constructor() { super(); this.record = { @@ -18,19 +19,19 @@ export class RecordFactory extends AbstractTypeFactory { }; } - static factory(): RecordFactory { - return new RecordFactory(); + static factory(): JetstreamRecordFactory { + return new JetstreamRecordFactory(); } - static make(): Record { - return RecordFactory.factory().create(); + static make(): JetstreamRecord { + return JetstreamRecordFactory.factory().create(); } - create(): Record { - return this.record as Record; + create(): JetstreamRecord { + return this.record as JetstreamRecord; } - type(inputType: CollectionType) { + type(inputType: JetstreamCollectionType) { this.record.$type = inputType; return this; } @@ -53,12 +54,54 @@ export class RecordFactory extends AbstractTypeFactory { return this; } - subject(inputSubject: Subject | string) { + subject(inputSubject: JetstreamSubject | string) { this.record.subject = inputSubject; return this; } } +export class NewSkeetRecordFactory extends AbstractTypeFactory { + public record: NewSkeetRecord; + constructor() { + super(); + this.record = { + $type: 'app.bsky.feed.post', + createdAt: Date.now().toString(), + embed: undefined, + facets: undefined, + langs: undefined, + text: 'example text', + reply: undefined, + }; + } + + static factory(): NewSkeetRecordFactory { + return new NewSkeetRecordFactory(); + } + + static make(): NewSkeetRecord { + return NewSkeetRecordFactory.factory().create(); + } + + create(): NewSkeetRecord { + return this.record as NewSkeetRecord; + } + + reply(reply: JetstreamReply | undefined = undefined) { + if (reply === undefined) { + this.record.reply = ReplyFactory.make(); + } else { + this.record.reply = reply; + } + return this; + } + + text(text: string) { + this.record.text = text; + return this; + } +} + /** * Represents a factory class for creating a skeet record object. * @extends AbstractTypeFactory @@ -106,9 +149,9 @@ export class CreateSkeetRecordFactory extends AbstractTypeFactory { } /** - * Set the text for the Skeet Record object. + * Set the text for the Skeet JetstreamRecord object. * - * @param {string} skeetText - The text to be set for the Skeet Record. + * @param {string} skeetText - The text to be set for the Skeet JetstreamRecord. * @return {CreateSkeetRecordFactory} - The update instance of CreateSkeetRecordFactory. */ text(skeetText: string): CreateSkeetRecordFactory { @@ -119,17 +162,17 @@ export class CreateSkeetRecordFactory extends AbstractTypeFactory { /** * Sets the reply value for a SkeetRecordFactory object. * - * @param {Reply} skeetReply - The reply value to be set for the SkeetRecordFactory object. + * @param {JetstreamReply} skeetReply - The reply value to be set for the SkeetRecordFactory object. * @return {CreateSkeetRecordFactory} - The update instance of CreateSkeetRecordFactory. */ - reply(skeetReply: Reply): CreateSkeetRecordFactory { + reply(skeetReply: JetstreamReply): CreateSkeetRecordFactory { this.skeetRecordObject.reply = skeetReply; return this; } } -export class SubjectFactory extends AbstractTypeFactory { - public subject: Subject; +export class JetstreamSubjectFactory extends AbstractTypeFactory { + public subject: JetstreamSubject; constructor() { super(); this.subject = { @@ -138,16 +181,16 @@ export class SubjectFactory extends AbstractTypeFactory { }; } - static factory(): SubjectFactory { - return new SubjectFactory(); + static factory(): JetstreamSubjectFactory { + return new JetstreamSubjectFactory(); } - static make(): Subject { - return SubjectFactory.factory().create(); + static make(): JetstreamSubject { + return JetstreamSubjectFactory.factory().create(); } - create(): Subject { - return this.subject as Subject; + create(): JetstreamSubject { + return this.subject as JetstreamSubject; } cid(inputCid: string) { @@ -162,11 +205,11 @@ export class SubjectFactory extends AbstractTypeFactory { } /** - * A factory class for creating Reply objects. + * A factory class for creating JetstreamReply objects. * @extends AbstractTypeFactory */ export class ReplyFactory extends AbstractTypeFactory { - public reply: Reply; + public reply: JetstreamReply; constructor() { super(); this.reply = { @@ -185,21 +228,21 @@ export class ReplyFactory extends AbstractTypeFactory { return new ReplyFactory(); } - static make(): Reply { + static make(): JetstreamReply { return ReplyFactory.factory().create(); } - create(): Reply { - return this.reply as Reply; + create(): JetstreamReply { + return this.reply as JetstreamReply; } /** * Sets the root subject for the reply. * - * @param {Subject} replyRoot - The root subject for the reply. + * @param {JetstreamSubject} replyRoot - The root subject for the reply. * @return {ReplyFactory} - The updated instance of the ReplyFactory. */ - root(replyRoot: Subject): ReplyFactory { + root(replyRoot: JetstreamSubject): ReplyFactory { this.reply.root = replyRoot; return this; } @@ -207,10 +250,10 @@ export class ReplyFactory extends AbstractTypeFactory { /** * Sets the parent reply for the given subject. * - * @param {Subject} replyParent - The parent reply to be set. + * @param {JetstreamSubject} replyParent - The parent reply to be set. * @return {ReplyFactory} - The updated instance of the ReplyFactory. */ - parent(replyParent: Subject): ReplyFactory { + parent(replyParent: JetstreamSubject): ReplyFactory { this.reply.parent = replyParent; return this; } diff --git a/src/utils/time-utils.ts b/src/utils/time-utils.ts index 370dd3a..60d7f2e 100644 --- a/src/utils/time-utils.ts +++ b/src/utils/time-utils.ts @@ -1,3 +1,5 @@ +import moment from 'moment-timezone'; + export function getHumanReadableDateTimeStamp( datetime: string, timezone: string = 'America/Chicago' @@ -25,3 +27,24 @@ export function nowDateTime() { minute: '2-digit', }); } + +export function getTimezonesWhereItIsAGivenTime(timeToCheck: string): string[] { + const format = 'HH:mm'; + + const allTimezones = moment.tz.names(); + const tz: string[] = []; + + allTimezones.forEach((timezone) => { + const currentTimeInTimezone = moment.tz(timezone).format(format); + if (currentTimeInTimezone === timeToCheck) { + tz.push(timezone); + } + }); + + return tz; +} + +export function isTimeInHHMMFormat(time: string) { + const pattern = /^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/; + return pattern.test(time); +} diff --git a/src/validations/AbstractValidator.ts b/src/validations/AbstractValidator.ts index 9a6cc2e..efd5395 100644 --- a/src/validations/AbstractValidator.ts +++ b/src/validations/AbstractValidator.ts @@ -1,38 +1,30 @@ -import { RepoOp } from '@atproto/api/dist/client/types/com/atproto/sync/subscribeRepos'; import { HandlerAgent } from '../agent/HandlerAgent'; -import { CreateSkeetMessage, JetstreamMessage } from '../types/JetstreamTypes'; export abstract class AbstractValidator { - private negate: boolean = false; + protected negate: boolean = false; constructor() {} static make(...args: any): AbstractValidator { throw new Error('Method Not Implemented! Use constructor.'); } - not(): AbstractValidator { + not(): this { this.negate = true; return this; } - getTextFromPost(message: JetstreamMessage): string { - const createSkeetMessage = message as CreateSkeetMessage; - const text = createSkeetMessage.record.text; - return text; - } - // @ts-ignore abstract async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + ...args: any ): Promise; // @ts-ignore async shouldTrigger( - message: JetstreamMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + ...args: any ): Promise { - const valid: boolean = await this.handle(message, handlerAgent); + const valid: boolean = await this.handle(handlerAgent, ...args); return this.negate ? !valid : valid; } } diff --git a/src/validations/GenericValidators.ts b/src/validations/GenericValidators.ts deleted file mode 100644 index 456a08a..0000000 --- a/src/validations/GenericValidators.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AbstractValidator } from './AbstractValidator'; -import { JetstreamMessage } from '../types/JetstreamTypes'; -import { HandlerAgent } from '../agent/HandlerAgent'; - -export class ActionTakenByUserValidator extends AbstractValidator { - constructor(private userDid: string) { - super(); - } - - static make(userDid: string): ActionTakenByUserValidator { - return new ActionTakenByUserValidator(userDid); - } - - async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent - ): Promise { - return this.userDid === message.did; - } -} diff --git a/src/validations/LogicalValidators.ts b/src/validations/LogicalValidators.ts index 05a884e..6a74c52 100644 --- a/src/validations/LogicalValidators.ts +++ b/src/validations/LogicalValidators.ts @@ -1,6 +1,5 @@ import { AbstractValidator } from './AbstractValidator'; import { HandlerAgent } from '../agent/HandlerAgent'; -import { JetstreamMessage } from '../types/JetstreamTypes'; /** * A validator in which you pass a single function that takes in the post @@ -9,8 +8,8 @@ import { JetstreamMessage } from '../types/JetstreamTypes'; export class SimpleFunctionValidator extends AbstractValidator { constructor( private triggerValidator: ( - arg0: JetstreamMessage, - arg1: HandlerAgent + arg0: HandlerAgent | undefined, + ...args: any ) => boolean | PromiseLike ) { super(); @@ -18,18 +17,18 @@ export class SimpleFunctionValidator extends AbstractValidator { static make( triggerValidator: ( - arg0: JetstreamMessage, - arg1: HandlerAgent + arg0: HandlerAgent | undefined, + ...args: any ) => boolean | PromiseLike ): SimpleFunctionValidator { return new SimpleFunctionValidator(triggerValidator); } async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent | undefined, + ...args: any ): Promise { - return await this.triggerValidator(message, handlerAgent); + return await this.triggerValidator(handlerAgent, ...args); } } @@ -46,15 +45,12 @@ export class OrValidator extends AbstractValidator { return new OrValidator(validators); } - async handle( - message: JetstreamMessage, - handlerAgent: HandlerAgent - ): Promise { + async handle(handlerAgent: HandlerAgent, ...args: any): Promise { let willTrigger = false; for (const validator of this.validators) { const currentValidatorWillTrigger = await validator.shouldTrigger( - message, - handlerAgent + handlerAgent, + ...args ); if (currentValidatorWillTrigger) { willTrigger = true; diff --git a/src/validations/README.md b/src/validations/README.md index 51dd8f1..95ee4be 100644 --- a/src/validations/README.md +++ b/src/validations/README.md @@ -14,6 +14,7 @@ Validators are used to determine whether an action should be triggered. We provi - [PostedByUserValidator](#postedbyuservalidator) - [ReplyingToBotValidator](#replyingtobotvalidator) - [IsReplyValidator](#isreplyvalidator) + - [IsNewPost](#isnewpost) - [String Validators](#string-validators) - [InputIsCommandValidator](#inputiscommandvalidator) - [InputStartsWithValidator](#inputstartswithvalidator) @@ -25,11 +26,19 @@ Validators are used to determine whether an action should be triggered. We provi - Follow - [Follow Validators](#follow-validators) - [NewFollowerForUserValidator](#newfollowerforuservalidator) + - [NewFollowFromUserValidator](#newfollowfromuservalidator) - [UserFollowedValidator](#userfollowedvalidator) - Like - - Coming soon + - [Like Validators](#like-validators) + - [PostLikesValidator](#postlikesvalidator) + - [LikeByUser](#likebyuser) + - [LikeOfUser](#likeofuser) + - [LikeOfPost](#likeofpost) - Repost - - Coming soon + - [Repost Validators](#repost-validators) + - [RepostByUser](#repostbyuser) + - [RepostOfUser](#repostofuser) + - [RepostOfPost](#repostofpost) - Testing - [Test Validator](#test-validator) @@ -67,8 +76,8 @@ export class ExampleValidator extends AbstractValidator { } async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + ...args: any ): Promise { // Perform validation // must return a boolean @@ -76,132 +85,282 @@ export class ExampleValidator extends AbstractValidator { } ``` -Any additional parameters you may need for the action can be passed into the constructor and used within the `handle` function as needed, like so - +Any additional parameters you may need for the action can be passed into the constructor and used within the handle function as needed, like so: ```typescript export class ExampleValidator extends AbstractValidator { constructor(private shouldPass: boolean) { super(); } - async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent - ): Promise { + async handle(handlerAgent: HandlerAgent, messsage: JetstreamEventCommit): Promise { // This example takes in a boolean, and returns it from should trigger. - return this.shouldPass; + return message.commit.operation === 'create' && this.shouldPass; } } ``` -## Logical validator +## Logical Validator ### SimpleFunctionValidator -The `SimpleFunctionValidator` class provides a way to create a validator by passing a single function that accepts the JetstreamMessage and HandlerAgent and returns a boolean indicating whether to trigger the action or not. - -`SimpleFunctionValidator.make((message, handlerAgent) => { return true; }); // replace function with specific condition` +The SimpleFunctionValidator class provides a way to create a validator by passing a single function that accepts the HandlerAgent and returns a boolean indicating whether to trigger the action or not. +```typescript +SimpleFunctionValidator.make((handlerAgent) => { return true; }); +``` +If it's being used for a message handler, you can expect the message parameter in the function as well +```typescript +SimpleFunctionValidator.make((handlerAgent, message) => { + // perform validation with message +}); +``` ### OrValidator -The `OrValidator` class allows you to pass in multiple validators. If any of these validators return `true`, it will trigger the action. - -`OrValidator.make([validator1, validator2, validator3]); // replace with actual validator instances` +The OrValidator class allows you to pass in multiple validators. If any of these validators return true, it will trigger the action. +```typescript +OrValidator.make([validator1, validator2, validator3]); // replace with actual validator instances +``` ## Generic Validators ### ActionTakenByUserValidator -The `ActionTakenByUserValidator` class checks if the action (post, repost, like, follow) was done by a given user - -`ActionTakenByUserValidator.make('did:plc:123');` +The ActionTakenByUserValidator class checks if the action (post, repost, etc.) was taken by a specified user. +```typescript +ActionTakenByUserValidator.make('userDid'); +``` -## Post validators +### Post Validators ### PostedByUserValidator -The `PostedByUserValidator` class checks if the post was made by a specific user, identified by their DID (Decentralized Identifier). - -`PostedByUserValidator.make('did:plc:123');` +The PostedByUserValidator checks if a post was created by a specific user. +```typescript +PostedByUserValidator.make('userDid'); +``` ### ReplyingToBotValidator -The `ReplyingToBotValidator` class verifies if the post is a reply to the bot/handlerAgent. - -`ReplyingToBotValidator.make();` +The ReplyingToBotValidator checks if a post is a reply to the bot. +```typescript +ReplyingToBotValidator.make(); +``` ### IsReplyValidator -The `IsReplyValidator` class checks if the post is a reply to another post. +The IsReplyValidator checks if a post is a reply. +```typescript +IsReplyValidator.make(); +``` -`IsReplyValidator.make();` +### IsNewPost -## String Validators +The IsNewPost validator checks if a post was created in the past 24 hours, this helps to skip posts that are imported from twitter with an import tool. +```typescript +IsNewPost.make(); +``` -### InputIsCommandValidator -The `InputIsCommandValidator` class validates if the input is a command triggered by a specific key. The `strict` argument enforces case sensitivity when set to `true`. +### String Validators -`InputIsCommandValidator.make('myTriggerKey', true); // enabling strict mode` +### InputIsCommandValidator -### InputStartsWithValidator +The InputIsCommandValidator checks if a post starts with a specific command trigger. +```typescript +InputIsCommandValidator.make('triggerKey', true); // strict mode +``` -The `InputStartsWithValidator` class checks if the input starts with a specific key. The `strict` argument, when set to `true`, enforces case sensitivity. +### InputStartsWithValidator -`InputStartsWithValidator.make('myTriggerKey', false);` +The InputStartsWithValidator checks if a post starts with a specific string. +```typescript +InputStartsWithValidator.make('triggerKey', true); // strict mode +``` ### InputContainsValidator -The `InputContainsValidator` class verifies if the input contains a specific key. The `strict` argument, when set to `true`, enforces case sensitivity. - -`InputContainsValidator.make('myTriggerKey', false);` +The InputContainsValidator checks if a post contains a specific string. +```typescript +InputContainsValidator.make('triggerKey', true); // strict mode +``` ### InputEqualsValidator -The `InputEqualsValidator` class checks if the input exactly matches a specific key. - -`InputEqualsValidator.make('myTriggerKey');` +The InputEqualsValidator checks if a post equals a specific string. +```typescript +InputEqualsValidator.make('triggerKey'); +``` -## Bot Validators +### Bot Validators ### IsGoodBotValidator -The `IsGoodBotValidator` class checks if the input is replying to the bot and the text is "{positive word} bot" (ex. good bot). +The IsGoodBotValidator checks if a post responds positively to the bot. +```typescript +IsGoodBotValidator.make(); +``` -It will also accept "thank you" (for full list of accepted inputs, see `isGoodBotResponse` in `utils/text-utils`) +### IsBadBotValidator -`IsGoodBotValidator.make();` +The IsBadBotValidator checks if a post responds negatively to the bot. +```typescript +IsBadBotValidator.make(); +``` -### IsBadBotValidator +### Follow Validators -The `IsBadBotValidator` class checks if the input is replying to the bot and the text is "{negative word} bot" (ex. bad bot). +### NewFollowerForUserValidator -(for full list of accepted inputs, see `isBadBotResponse` in `utils/text-utils`) +The NewFollowerForUserValidator checks if a user has a new follower. +```typescript +NewFollowerForUserValidator.make('userDid'); +``` -`IsBadBotValidator.make();` +### NewFollowFromUserValidator -## Follow Validators +The NewFollowFromUserValidator checks if a user has followed someone. +```typescript +NewFollowFromUserValidator.make('userDid'); +``` -### NewFollowerForUserValidator +### UserFollowedValidator + +The UserFollowedValidator (alias for NewFollowFromUserValidator) checks if a user has followed someone. (To be deprecated in next major release) +```typescript +UserFollowedValidator.make('userDid'); +``` -The `NewFollowerForUserValidator` will return true if the follow action was a new follower for the given user -If no did is provided, it will default to the bot/handlerAgent did -`NewFollowerForUserValidator.make('did:plc:123');` +### Like Validators -### NewFollowFromUserValidator +#### PostLikesValidator -The `NewFollowFromUserValidator` will return true if the follow action was the given user following someone -If no did is provided, it will default to the bot/handlerAgent did +The `PostLikesValidator` checks if a post's like count matches certain criteria, such as being equal to, greater than, less than, or between specified values. -`NewFollowFromUserValidator.make('did:plc:123');` +```typescript +PostLikesValidator.make( + postUri: string, + comparisonType: 'equal' | 'greaterThan' | 'lessThan' | 'between', + likeCount?: number, // Required for 'equal', 'greaterThan', 'lessThan' + likeCountMin?: number, // Required for 'between' + likeCountMax?: number // Required for 'between' +) +``` +- **postUri**: The URI of the post to be checked. +- **comparisonType**: The type of comparison to perform (`'equal'`, `'greaterThan'`, `'lessThan'`, `'between'`). +- **likeCount**: The like count to compare against (optional for `'between'`). +- **likeCountMin**: The minimum like count for the `'between'` comparison. +- **likeCountMax**: The maximum like count for the `'between'` comparison. + +```typescript +// Validate if the number of likes on a post is exactly 100 +PostLikesValidator.make('postUri', 'equal', 100); + +// Validate if the number of likes on a post is greater than 50 +PostLikesValidator.make('postUri', 'greaterThan', 50); + +// Validate if the number of likes on a post is less than 10 +PostLikesValidator.make('postUri', 'lessThan', 10); + +// Validate if the number of likes on a post is between 20 and 30 +PostLikesValidator.make('postUri', 'between', undefined, 20, 30); +``` + +#### LikeByUser + +The `LikeByUser` validator checks if a specified user has liked a particular post. It can be configured to either validate likes from the bot user or a specific user if a user DID is provided. + +```typescript +// Validate if a specific user liked a specific post +import { LikeByUser } from './LikeUserValidators'; + +LikeByUser.make('userDid123', 'postUri'); + +// Validate if the current handlerAgent user liked a specific post +LikeByUser.make(undefined, 'postUri'); -**Was previously `UserFollowedValidator` (which still works for now) but has been renamed to fit with the other follow validators** +// This is the same behavior as above +LikeByUser.make(handlerAgent.getDid, 'postUri') +``` + +#### LikeOfUser + +The `LikeOfUser` validator ensures that a post liked by someone is from a specified user. + +```typescript +// Validate if a post from a specific user was liked +LikeOfUser.make('userDid123', undefined); + +// Validate if a specific post from any user was liked +LikeOfUser.make(undefined, 'postUri'); +``` -## Test Validator +#### LikeOfPost -### TestValidator +The `LikeOfPost` validator checks if the event is a like on a specific post. -The `TestValidator` class accepts a boolean in the constructor, and then returns that boolean when validated. Mostly used for testing +```typescript +// Validate if a specific post has been liked +LikeOfPost.make('postUri'); +``` + +### Repost Validators + +#### RepostByUser + +The `RepostByUser` validator checks if a specified user has reposted a particular post. + +```typescript +// Validate if a specific user reposted a specific post +RepostByUser.make('userDid123', 'postUri'); -`TestValidator.make(true|false);` +// Validate if the bot user reposted a specific post +RepostByUser.make(undefined, 'postUri'); // same as make(handlerAgent.getDid, 'postUri') +``` + +#### RepostOfUser + +The `RepostOfUser` validator ensures that the reposted post is from a specific user + +```typescript +// Validate if a post from a specific user was reposted +RepostOfUser.make('userDid123', undefined); + +// Validate if a specific post from the bot user was reposted +RepostOfUser.make(undefined, 'postUri'); +``` + + +#### RepostOfPost + +The `RepostOfPost` validator checks if reposts are directed towards a specific post + +```typescript +// Validate if a specific post has been reposted +RepostOfPost.make('postUri'); +``` + +## Testing + +### Test Validator + +The TestValidator is used for unit testing your validator logic. +```typescript +TestValidator.make(true); // Returns an instance that will pass +``` + +### Specialized Validators + +### IsSpecifiedTimeValidator + +The IsSpecifiedTimeValidator checks if the current time matches any specified times. +```typescript +IsSpecifiedTimeValidator.make('HH:MM', 'HH:MM'); +``` + +### IsFourTwentyValidator + +The IsFourTwentyValidator checks if the current time is 4:20 AM or PM in any timezone. +```typescript +IsFourTwentyValidator.make(); +``` diff --git a/src/validations/TestValidator.ts b/src/validations/TestValidator.ts index 2b261f3..4e54fbc 100644 --- a/src/validations/TestValidator.ts +++ b/src/validations/TestValidator.ts @@ -1,6 +1,5 @@ import { AbstractValidator } from './AbstractValidator'; import { HandlerAgent } from '../agent/HandlerAgent'; -import { CreateSkeetMessage } from '../types/JetstreamTypes'; export class TestValidator extends AbstractValidator { constructor(private shouldPass: boolean) { @@ -11,10 +10,7 @@ export class TestValidator extends AbstractValidator { return new TestValidator(shouldPass); } - async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent - ): Promise { + async handle(handlerAgent: HandlerAgent): Promise { return this.shouldPass; } } diff --git a/src/validations/interval-validators/IsFourTwentyValidator.ts b/src/validations/interval-validators/IsFourTwentyValidator.ts new file mode 100644 index 0000000..0129887 --- /dev/null +++ b/src/validations/interval-validators/IsFourTwentyValidator.ts @@ -0,0 +1,33 @@ +import { AbstractValidator } from '../AbstractValidator'; +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { getTimezonesWhereItIsAGivenTime } from '../../utils/time-utils'; + +export class IsFourTwentyValidator extends AbstractValidator { + protected timezones: string[] = []; + protected matchingTimezones: string[] = []; + + constructor() { + super(); + } + + static make(): IsFourTwentyValidator { + return new IsFourTwentyValidator(); + } + + async handle(handlerAgent: HandlerAgent): Promise { + const timezones = + IsFourTwentyValidator.getTimezonesWhereItIsFourTwenty(); + + return timezones.totalTimezones > 0; + } + + static getTimezonesWhereItIsFourTwenty() { + const timezonesAM: string[] = getTimezonesWhereItIsAGivenTime('04:20'); + const timezonesPM: string[] = getTimezonesWhereItIsAGivenTime('16:20'); + return { + timezonesAM: timezonesAM, + timezonesPM: timezonesPM, + totalTimezones: timezonesAM.length + timezonesPM.length, + }; + } +} diff --git a/src/validations/interval-validators/IsSpecifiedTimeValidator.ts b/src/validations/interval-validators/IsSpecifiedTimeValidator.ts new file mode 100644 index 0000000..93c1d86 --- /dev/null +++ b/src/validations/interval-validators/IsSpecifiedTimeValidator.ts @@ -0,0 +1,43 @@ +import { AbstractValidator } from '../AbstractValidator'; +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { + getTimezonesWhereItIsAGivenTime, + isTimeInHHMMFormat, +} from '../../utils/time-utils'; +import { DebugLog } from '../../utils/DebugLog'; + +export class IsSpecifiedTimeValidator extends AbstractValidator { + protected timezones: string[] = []; + protected matchingTimezones: string[] = []; + + protected times: string[]; + constructor(...times: string[]) { + super(); + this.times = times; + } + + static make(...times: string[]): IsSpecifiedTimeValidator { + return new IsSpecifiedTimeValidator(...times); + } + + async handle(handlerAgent: HandlerAgent): Promise { + const timezones: { [key: string]: any; totalTimezones: number } = { + totalTimezones: 0, + }; + this.times.forEach((time) => { + if (!isTimeInHHMMFormat(time)) { + DebugLog.error( + 'Time Validator', + `${time} is not in a valid format` + ); + return; + } + + timezones[time] = getTimezonesWhereItIsAGivenTime(time); + timezones.totalTimezones = + timezones.totalTimezones + timezones[time].length; + }); + + return timezones.totalTimezones > 0; + } +} diff --git a/src/validations/message-validators/AbstractMessageValidator.ts b/src/validations/message-validators/AbstractMessageValidator.ts new file mode 100644 index 0000000..dc3bb69 --- /dev/null +++ b/src/validations/message-validators/AbstractMessageValidator.ts @@ -0,0 +1,34 @@ +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { JetstreamEvent, NewSkeetRecord } from '../../types/JetstreamTypes'; +import { AbstractValidator } from '../AbstractValidator'; + +export abstract class AbstractMessageValidator extends AbstractValidator { + constructor() { + super(); + } + + static make(...args: any): AbstractMessageValidator { + throw new Error('Method Not Implemented! Use constructor.'); + } + + getTextFromPost(message: JetstreamEvent): string { + const createSkeetMessage = message?.commit?.record as NewSkeetRecord; + const text = createSkeetMessage?.text; + return text; + } + + // @ts-ignore + abstract async handle( + handlerAgent: HandlerAgent, + message: JetstreamEvent + ): Promise; + + // @ts-ignore + async shouldTrigger( + handlerAgent: HandlerAgent, + message: JetstreamEvent + ): Promise { + const valid: boolean = await this.handle(handlerAgent, message); + return this.negate ? !valid : valid; + } +} diff --git a/src/validations/BotValidators.ts b/src/validations/message-validators/BotValidators.ts similarity index 57% rename from src/validations/BotValidators.ts rename to src/validations/message-validators/BotValidators.ts index bc21cc1..d41b941 100644 --- a/src/validations/BotValidators.ts +++ b/src/validations/message-validators/BotValidators.ts @@ -1,9 +1,9 @@ -import { isBadBotResponse, isGoodBotResponse } from '../utils/text-utils'; -import { AbstractValidator } from './AbstractValidator'; -import { HandlerAgent } from '../agent/HandlerAgent'; -import { CreateSkeetMessage } from '../types/JetstreamTypes'; +import { isBadBotResponse, isGoodBotResponse } from '../../utils/text-utils'; +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { JetstreamEventCommit } from '../../types/JetstreamTypes'; +import { AbstractMessageValidator } from './AbstractMessageValidator'; -export class IsGoodBotValidator extends AbstractValidator { +export class IsGoodBotValidator extends AbstractMessageValidator { constructor() { super(); } @@ -13,24 +13,24 @@ export class IsGoodBotValidator extends AbstractValidator { } async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { if (!handlerAgent.hasPostReply(message)) { return false; } const replyingToDid = handlerAgent.getDIDFromUri( // @ts-ignore - message.record.reply?.parent.uri + message.commit.record.reply?.parent.uri ); const isReplyToBot = handlerAgent.getDid === replyingToDid && - message.collection == 'app.bsky.feed.post'; + message.commit.collection == 'app.bsky.feed.post'; return isGoodBotResponse(this.getTextFromPost(message)) && isReplyToBot; } } -export class IsBadBotValidator extends AbstractValidator { +export class IsBadBotValidator extends AbstractMessageValidator { constructor() { super(); } @@ -40,19 +40,19 @@ export class IsBadBotValidator extends AbstractValidator { } async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { if (!handlerAgent.hasPostReply(message)) { return false; } const replyingToDid = handlerAgent.getDIDFromUri( // @ts-ignore - message.record.reply?.parent.uri + message.commit.record.reply?.parent.uri ); const isReplyToBot = handlerAgent.getDid === replyingToDid && - message.collection == 'app.bsky.feed.post'; + message.commit.collection == 'app.bsky.feed.post'; return isBadBotResponse(this.getTextFromPost(message)) && isReplyToBot; } } diff --git a/src/validations/message-validators/GenericValidators.ts b/src/validations/message-validators/GenericValidators.ts new file mode 100644 index 0000000..71ae093 --- /dev/null +++ b/src/validations/message-validators/GenericValidators.ts @@ -0,0 +1,20 @@ +import { JetstreamEventCommit } from '../../types/JetstreamTypes'; +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { AbstractMessageValidator } from './AbstractMessageValidator'; + +export class ActionTakenByUserValidator extends AbstractMessageValidator { + constructor(private userDid: string) { + super(); + } + + static make(userDid: string): ActionTakenByUserValidator { + return new ActionTakenByUserValidator(userDid); + } + + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + return this.userDid === message.did; + } +} diff --git a/src/validations/message-validators/TestMessageValidator.ts b/src/validations/message-validators/TestMessageValidator.ts new file mode 100644 index 0000000..3aaf3ac --- /dev/null +++ b/src/validations/message-validators/TestMessageValidator.ts @@ -0,0 +1,20 @@ +import { AbstractMessageValidator } from './AbstractMessageValidator'; +import { HandlerAgent } from '../../agent/HandlerAgent'; +import { JetstreamEventCommit } from '../../types/JetstreamTypes'; + +export class TestMessageValidator extends AbstractMessageValidator { + constructor(private shouldPass: boolean) { + super(); + } + + static make(shouldPass: boolean): TestMessageValidator { + return new TestMessageValidator(shouldPass); + } + + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + return this.shouldPass; + } +} diff --git a/src/validations/follow/FollowValidators.ts b/src/validations/message-validators/follow/FollowValidators.ts similarity index 59% rename from src/validations/follow/FollowValidators.ts rename to src/validations/message-validators/follow/FollowValidators.ts index 46f3666..892e711 100644 --- a/src/validations/follow/FollowValidators.ts +++ b/src/validations/message-validators/follow/FollowValidators.ts @@ -1,8 +1,8 @@ -import { CreateMessage, JetstreamMessage } from '../../types/JetstreamTypes'; -import { AbstractValidator } from '../AbstractValidator'; -import { HandlerAgent } from '../../agent/HandlerAgent'; +import { JetstreamEventCommit } from '../../../types/JetstreamTypes'; +import { HandlerAgent } from '../../../agent/HandlerAgent'; +import { AbstractMessageValidator } from '../AbstractMessageValidator'; -export class NewFollowerForUserValidator extends AbstractValidator { +export class NewFollowerForUserValidator extends AbstractMessageValidator { constructor(private userDid: string | undefined) { super(); } @@ -14,17 +14,17 @@ export class NewFollowerForUserValidator extends AbstractValidator { } async handle( - message: CreateMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { if (!this.userDid) { - return handlerAgent.getDid === message.record.subject; + return handlerAgent.getDid === message?.commit?.record?.subject; } - return this.userDid === message.record.subject; + return this.userDid === message?.commit?.record?.subject; } } -export class NewFollowFromUserValidator extends AbstractValidator { +export class NewFollowFromUserValidator extends AbstractMessageValidator { constructor(private userDid: string | undefined) { super(); } @@ -35,8 +35,8 @@ export class NewFollowFromUserValidator extends AbstractValidator { return new NewFollowFromUserValidator(userDid); } async handle( - message: CreateMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { if (!this.userDid) { return handlerAgent.getDid === message.did; diff --git a/src/validations/message-validators/like/LikeCountValidators.ts b/src/validations/message-validators/like/LikeCountValidators.ts new file mode 100644 index 0000000..e24cde6 --- /dev/null +++ b/src/validations/message-validators/like/LikeCountValidators.ts @@ -0,0 +1,72 @@ +import { JetstreamEventCommit } from '../../../types/JetstreamTypes'; +import { HandlerAgent } from '../../../agent/HandlerAgent'; +import { AbstractMessageValidator } from '../AbstractMessageValidator'; + +type ComparisonType = 'equal' | 'greaterThan' | 'lessThan' | 'between'; + +export class PostLikesValidator extends AbstractMessageValidator { + constructor( + private postUri: string, + private comparisonType: ComparisonType, + private likeCount?: number, // Optional for 'between' + private likeCountMin?: number, // Required for 'between' + private likeCountMax?: number // Required for 'between' + ) { + super(); + if (this.comparisonType !== 'between' && this.likeCount === undefined) { + throw new Error( + 'likeCount is required for non-between comparisons' + ); + } + + if ( + this.comparisonType === 'between' && + (this.likeCountMin === undefined || this.likeCountMax === undefined) + ) { + throw new Error( + 'likeCountMin and likeCountMax are required for between comparisons' + ); + } + } + + static make( + postUri: string, + comparisonType: ComparisonType, + likeCount?: number, + likeCountMin?: number, + likeCountMax?: number + ): PostLikesValidator { + return new PostLikesValidator( + postUri, + comparisonType, + likeCount, + likeCountMin, + likeCountMax + ); + } + + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + const likes = await handlerAgent.getPostLikeCount(this.postUri); + + switch (this.comparisonType) { + case 'equal': + return likes === this.likeCount; + case 'greaterThan': + // @ts-ignore + return likes > this.likeCount; + case 'lessThan': + // @ts-ignore + return likes < this.likeCount; + case 'between': + return ( + // @ts-ignore + likes >= this.likeCountMin && + // @ts-ignore + likes <= this.likeCountMax + ); + } + } +} diff --git a/src/validations/message-validators/like/LikeUserValidators.ts b/src/validations/message-validators/like/LikeUserValidators.ts new file mode 100644 index 0000000..306e6d8 --- /dev/null +++ b/src/validations/message-validators/like/LikeUserValidators.ts @@ -0,0 +1,94 @@ +import { JetstreamEventCommit } from '../../../types/JetstreamTypes'; +import { HandlerAgent } from '../../../agent/HandlerAgent'; +import { AbstractMessageValidator } from '../AbstractMessageValidator'; + +export class LikeByUser extends AbstractMessageValidator { + constructor( + private userDid: string | undefined, + private postUri: string | undefined + ) { + super(); + } + + static make( + userDid: string | undefined = undefined, + postUri: string | undefined = undefined + ): LikeByUser { + return new LikeByUser(userDid, postUri); + } + + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + if (!message.commit.record?.subject) return false; + if (typeof message.commit.record?.subject == 'string') return false; + + const uri = message.commit.record?.subject.uri; + + if (this.postUri) { + if (uri != this.postUri) return false; + } + + if (!this.userDid) { + return handlerAgent.getDid === message.did; + } + return this.userDid === message.did; + } +} + +export class LikeOfUser extends AbstractMessageValidator { + constructor( + private userDid: string | undefined, + private postUri: string | undefined + ) { + super(); + } + + static make( + userDid: string | undefined = undefined, + postUri: string | undefined = undefined + ): LikeOfUser { + return new LikeOfUser(userDid, postUri); + } + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + if (!message.commit.record?.subject) return false; + if (typeof message.commit.record?.subject == 'string') return false; + + const uri = message.commit.record?.subject.uri; + + if (this.postUri) { + if (uri != this.postUri) return false; + } + + const postDid = handlerAgent.getDIDFromUri(uri); + if (!this.userDid) { + return handlerAgent.getDid === postDid; + } + return this.userDid === postDid; + } +} + +export class LikeOfPost extends AbstractMessageValidator { + constructor(private postUri: string) { + super(); + } + + static make(postUri: string): LikeOfPost { + return new LikeOfPost(postUri); + } + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + if (typeof message.commit.record?.subject == 'string') return false; + if (!message.commit.record?.subject) return false; + + const uri = message.commit.record?.subject.uri; + + return uri == this.postUri; + } +} diff --git a/src/validations/message-validators/post/PostValidators.ts b/src/validations/message-validators/post/PostValidators.ts new file mode 100644 index 0000000..224a31f --- /dev/null +++ b/src/validations/message-validators/post/PostValidators.ts @@ -0,0 +1,87 @@ +import { HandlerAgent } from '../../../agent/HandlerAgent'; +import { JetstreamEventCommit } from '../../../types/JetstreamTypes'; +import { AbstractMessageValidator } from '../AbstractMessageValidator'; + +export class PostedByUserValidator extends AbstractMessageValidator { + constructor(private userDid: string) { + super(); + } + + static make(userDid: string): PostedByUserValidator { + return new PostedByUserValidator(userDid); + } + + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + return ( + this.userDid === message.did && + message.commit.collection == 'app.bsky.feed.post' + ); + } +} + +export class ReplyingToBotValidator extends AbstractMessageValidator { + constructor() { + super(); + } + + static make(): ReplyingToBotValidator { + return new ReplyingToBotValidator(); + } + + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + if (!message.commit.record?.reply) return false; + const replyingToDid = handlerAgent.getDIDFromUri( + message.commit.record.reply?.parent.uri + ); + + return ( + handlerAgent.getDid === replyingToDid && + message.commit.collection == 'app.bsky.feed.post' + ); + } +} + +export class IsReplyValidator extends AbstractMessageValidator { + constructor() { + super(); + } + + static make(): IsReplyValidator { + return new IsReplyValidator(); + } + + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + return handlerAgent.hasPostReply(message); + } +} + +export class IsNewPost extends AbstractMessageValidator { + constructor() { + super(); + } + + static make(): IsNewPost { + return new IsNewPost(); + } + + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + if (!message.commit.record) return false; + const createdAt = new Date(message?.commit.record?.createdAt); + const now = new Date(); + const oneDay = 24 * 60 * 60 * 1000; + + return now.getTime() - createdAt.getTime() < oneDay; + } +} diff --git a/src/validations/post/StringValidators.ts b/src/validations/message-validators/post/StringValidators.ts similarity index 75% rename from src/validations/post/StringValidators.ts rename to src/validations/message-validators/post/StringValidators.ts index ef5af09..ad922f4 100644 --- a/src/validations/post/StringValidators.ts +++ b/src/validations/message-validators/post/StringValidators.ts @@ -1,9 +1,9 @@ -import { flattenTextUpdated } from '../../utils/text-utils'; -import { AbstractValidator } from '../AbstractValidator'; -import { HandlerAgent } from '../../agent/HandlerAgent'; -import { CreateSkeetMessage } from '../../types/JetstreamTypes'; +import { flattenTextUpdated } from '../../../utils/text-utils'; +import { HandlerAgent } from '../../../agent/HandlerAgent'; +import { JetstreamEventCommit } from '../../../types/JetstreamTypes'; +import { AbstractMessageValidator } from '../AbstractMessageValidator'; -export class InputIsCommandValidator extends AbstractValidator { +export class InputIsCommandValidator extends AbstractMessageValidator { constructor( private triggerKey: string, private strict: boolean = true @@ -19,8 +19,8 @@ export class InputIsCommandValidator extends AbstractValidator { } async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { if (this.strict) { const input = this.getTextFromPost(message); @@ -39,7 +39,7 @@ export class InputIsCommandValidator extends AbstractValidator { } } -export class InputStartsWithValidator extends AbstractValidator { +export class InputStartsWithValidator extends AbstractMessageValidator { constructor( private triggerKey: string, private strict: boolean = false @@ -55,8 +55,8 @@ export class InputStartsWithValidator extends AbstractValidator { } async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { const input = this.getTextFromPost(message); if (this.strict) { @@ -67,7 +67,7 @@ export class InputStartsWithValidator extends AbstractValidator { } } -export class InputContainsValidator extends AbstractValidator { +export class InputContainsValidator extends AbstractMessageValidator { constructor( private triggerKey: string, private strict: boolean = false @@ -83,8 +83,8 @@ export class InputContainsValidator extends AbstractValidator { } async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { const input = this.getTextFromPost(message); if (this.strict) { @@ -95,7 +95,7 @@ export class InputContainsValidator extends AbstractValidator { } } -export class InputEqualsValidator extends AbstractValidator { +export class InputEqualsValidator extends AbstractMessageValidator { constructor(private triggerKey: string) { super(); } @@ -105,8 +105,8 @@ export class InputEqualsValidator extends AbstractValidator { } async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent + handlerAgent: HandlerAgent, + message: JetstreamEventCommit ): Promise { const input = this.getTextFromPost(message); return input === this.triggerKey; diff --git a/src/validations/message-validators/repost/RepostValidators.ts b/src/validations/message-validators/repost/RepostValidators.ts new file mode 100644 index 0000000..565be2d --- /dev/null +++ b/src/validations/message-validators/repost/RepostValidators.ts @@ -0,0 +1,94 @@ +import { JetstreamEventCommit } from '../../../types/JetstreamTypes'; +import { HandlerAgent } from '../../../agent/HandlerAgent'; +import { AbstractMessageValidator } from '../AbstractMessageValidator'; + +export class RepostByUser extends AbstractMessageValidator { + constructor( + private userDid: string | undefined, + private postUri: string | undefined + ) { + super(); + } + + static make( + userDid: string | undefined = undefined, + postUri: string | undefined = undefined + ): RepostByUser { + return new RepostByUser(userDid, postUri); + } + + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + if (!message.commit.record?.subject) return false; + if (typeof message.commit.record?.subject == 'string') return false; + + const uri = message.commit.record?.subject.uri; + + if (this.postUri) { + if (uri != this.postUri) return false; + } + + if (!this.userDid) { + return handlerAgent.getDid === message.did; + } + return this.userDid === message.did; + } +} + +export class RepostOfUser extends AbstractMessageValidator { + constructor( + private userDid: string | undefined, + private postUri: string | undefined + ) { + super(); + } + + static make( + userDid: string | undefined = undefined, + postUri: string | undefined = undefined + ): RepostOfUser { + return new RepostOfUser(userDid, postUri); + } + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + if (!message.commit.record?.subject) return false; + if (typeof message.commit.record?.subject == 'string') return false; + + const uri = message.commit.record?.subject.uri; + + if (this.postUri) { + if (uri != this.postUri) return false; + } + + const postDid = handlerAgent.getDIDFromUri(uri); + if (!this.userDid) { + return handlerAgent.getDid === postDid; + } + return this.userDid === postDid; + } +} + +export class RepostOfPost extends AbstractMessageValidator { + constructor(private postUri: string) { + super(); + } + + static make(postUri: string): RepostOfPost { + return new RepostOfPost(postUri); + } + async handle( + handlerAgent: HandlerAgent, + message: JetstreamEventCommit + ): Promise { + if (typeof message.commit.record?.subject == 'string') return false; + if (!message.commit.record?.subject) return false; + + const uri = message.commit.record?.subject.uri; + + return uri == this.postUri; + } +} diff --git a/src/validations/post/PostValidators.ts b/src/validations/post/PostValidators.ts deleted file mode 100644 index a625e53..0000000 --- a/src/validations/post/PostValidators.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { AbstractValidator } from '../AbstractValidator'; -import { HandlerAgent } from '../../agent/HandlerAgent'; -import { CreateSkeetMessage } from '../../types/JetstreamTypes'; - -export class PostedByUserValidator extends AbstractValidator { - constructor(private userDid: string) { - super(); - } - - static make(userDid: string): PostedByUserValidator { - return new PostedByUserValidator(userDid); - } - - async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent - ): Promise { - return ( - this.userDid === message.did && - message.collection == 'app.bsky.feed.post' - ); - } -} - -export class ReplyingToBotValidator extends AbstractValidator { - constructor() { - super(); - } - - static make(): ReplyingToBotValidator { - return new ReplyingToBotValidator(); - } - - async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent - ): Promise { - if (!handlerAgent.hasPostReply(message)) { - return false; - } - const replyingToDid = handlerAgent.getDIDFromUri( - // @ts-ignore - message.record.reply?.parent.uri - ); - - return ( - handlerAgent.getDid === replyingToDid && - message.collection == 'app.bsky.feed.post' - ); - } -} - -export class IsReplyValidator extends AbstractValidator { - constructor() { - super(); - } - - static make(): IsReplyValidator { - return new IsReplyValidator(); - } - - async handle( - message: CreateSkeetMessage, - handlerAgent: HandlerAgent - ): Promise { - return handlerAgent.hasPostReply(message); - } -} diff --git a/tests/actions/FunctionAction.test.ts b/tests/actions/FunctionAction.test.ts index 1584f48..46f9b48 100644 --- a/tests/actions/FunctionAction.test.ts +++ b/tests/actions/FunctionAction.test.ts @@ -2,8 +2,9 @@ import { DebugLog, FunctionAction, HandlerAgent, + JetstreamEventCommit, + JetstreamEventFactory, JetstreamMessage, - JetstreamMessageFactory, nowDateTime, } from '../../src'; import mocked = jest.mocked; @@ -11,8 +12,9 @@ import mocked = jest.mocked; describe('FunctionAction', () => { const mockHandlerAgent = {} as HandlerAgent; - const mockMessage: JetstreamMessage = - JetstreamMessageFactory.factory().create(); + const mockMessage: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit() + .create() as JetstreamEventCommit; let mockActionFunction = jest.fn(); let functionAction: FunctionAction; @@ -25,11 +27,11 @@ describe('FunctionAction', () => { describe('handle', () => { it('runs provided function with proper arguments', async () => { - await functionAction.handle(mockMessage, mockHandlerAgent); + await functionAction.handle(mockHandlerAgent, mockMessage); expect(mockActionFunction).toHaveBeenCalledWith( - mockMessage, - mockHandlerAgent + mockHandlerAgent, + mockMessage ); }); }); @@ -41,8 +43,9 @@ describe('FunctionAction With DebugLog', () => { })); const mockHandlerAgent = {} as HandlerAgent; - const mockMessage: JetstreamMessage = - JetstreamMessageFactory.factory().create(); + const mockMessage: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit() + .create() as JetstreamEventCommit; beforeEach(() => { jest.clearAllMocks(); // clearing mocks @@ -57,11 +60,14 @@ describe('FunctionAction With DebugLog', () => { mocked(process.env, { shallow: true }).DEBUG_LOG_LEVEL = 'debug'; const consoleSpy = jest.spyOn(console, 'log'); const functionAction = new FunctionAction( - (message: JetstreamMessage, handlerAgent: HandlerAgent) => { + ( + handlerAgent: HandlerAgent | undefined, + message: JetstreamMessage + ) => { DebugLog.log('TEST', 'log'); } ); - await functionAction.handle(mockMessage, mockHandlerAgent); + await functionAction.handle(mockHandlerAgent, mockMessage); expect(consoleSpy).toHaveBeenCalledWith( `${nowDateTime()} | TEST | DEBUG | log` @@ -73,11 +79,14 @@ describe('FunctionAction With DebugLog', () => { mocked(process.env, { shallow: true }).DEBUG_LOG_LEVEL = 'debug'; const consoleSpy = jest.spyOn(console, 'log'); const functionAction = new FunctionAction( - (message: JetstreamMessage, handlerAgent: HandlerAgent) => { + ( + handlerAgent: HandlerAgent | undefined, + message: JetstreamMessage + ) => { DebugLog.debug('TEST', 'log'); } ); - await functionAction.handle(mockMessage, mockHandlerAgent); + await functionAction.handle(mockHandlerAgent, mockMessage); expect(consoleSpy).toHaveBeenCalledWith( `${nowDateTime()} | TEST | DEBUG | log` @@ -89,11 +98,14 @@ describe('FunctionAction With DebugLog', () => { mocked(process.env, { shallow: true }).DEBUG_LOG_LEVEL = 'info'; const consoleSpy = jest.spyOn(console, 'log'); const functionAction = new FunctionAction( - (message: JetstreamMessage, handlerAgent: HandlerAgent) => { + ( + handlerAgent: HandlerAgent | undefined, + message: JetstreamMessage + ) => { DebugLog.info('TEST', 'log'); } ); - await functionAction.handle(mockMessage, mockHandlerAgent); + await functionAction.handle(mockHandlerAgent, mockMessage); expect(consoleSpy).toHaveBeenCalledWith( `${nowDateTime()} | TEST | INFO | log` @@ -105,11 +117,14 @@ describe('FunctionAction With DebugLog', () => { mocked(process.env, { shallow: true }).DEBUG_LOG_LEVEL = 'info'; const consoleSpy = jest.spyOn(console, 'log'); const functionAction = new FunctionAction( - (message: JetstreamMessage, handlerAgent: HandlerAgent) => { + ( + handlerAgent: HandlerAgent | undefined, + message: JetstreamMessage + ) => { DebugLog.warn('TEST', 'log'); } ); - await functionAction.handle(mockMessage, mockHandlerAgent); + await functionAction.handle(mockHandlerAgent, mockMessage); expect(consoleSpy).toHaveBeenCalledWith( `${nowDateTime()} | TEST | WARN | log` @@ -121,11 +136,14 @@ describe('FunctionAction With DebugLog', () => { mocked(process.env, { shallow: true }).DEBUG_LOG_LEVEL = 'info'; const consoleSpy = jest.spyOn(console, 'log'); const functionAction = new FunctionAction( - (message: JetstreamMessage, handlerAgent: HandlerAgent) => { + ( + handlerAgent: HandlerAgent | undefined, + message: JetstreamMessage + ) => { DebugLog.error('TEST', 'log'); } ); - await functionAction.handle(mockMessage, mockHandlerAgent); + await functionAction.handle(mockHandlerAgent, mockMessage); expect(consoleSpy).toHaveBeenCalledWith( `${nowDateTime()} | TEST | ERROR | log` diff --git a/tests/actions/LoggingActions.test.ts b/tests/actions/LoggingActions.test.ts index 572a5b2..9c03ef5 100644 --- a/tests/actions/LoggingActions.test.ts +++ b/tests/actions/LoggingActions.test.ts @@ -1,47 +1,26 @@ import { DebugLogAction, HandlerAgent, - JetstreamMessage, - JetstreamMessageFactory, + JetstreamEventCommit, + JetstreamEventFactory, LogInputTextAction, - LogMessageAction, } from '../../src'; import { advanceTo } from 'jest-date-mock'; import mocked = jest.mocked; -describe('LogMessageAction', () => { - let action: LogMessageAction; - let handlerAgent: HandlerAgent; - let message: JetstreamMessage; - console.log = jest.fn(); - - beforeEach(() => { - handlerAgent = {} as HandlerAgent; - message = JetstreamMessageFactory.factory().create(); - action = LogMessageAction.make(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('Should log output of RepoOp object when handle() is called', async () => { - await action.handle(message, handlerAgent); - expect(console.log).toHaveBeenCalledWith(message); - }); -}); - describe('LogInputTextAction', () => { let input: string; let action: LogInputTextAction; let handlerAgent: HandlerAgent; - let message: JetstreamMessage; + let message: JetstreamEventCommit; console.log = jest.fn(); beforeEach(() => { input = 'hello'; handlerAgent = {} as HandlerAgent; - message = JetstreamMessageFactory.factory().create(); + message = JetstreamEventFactory.factory() + .commit() + .create() as JetstreamEventCommit; action = LogInputTextAction.make(input); }); @@ -50,7 +29,7 @@ describe('LogInputTextAction', () => { }); it('Should log output of RepoOp object when handle() is called', async () => { - await action.handle(message, handlerAgent); + await action.handle(handlerAgent, message); expect(console.log).toHaveBeenCalledWith(input); }); }); @@ -58,8 +37,9 @@ describe('LogInputTextAction', () => { describe('LogInputTextAction', () => { let action: DebugLogAction; const handlerAgent: HandlerAgent = {} as HandlerAgent; - const message: JetstreamMessage = - JetstreamMessageFactory.factory().create(); + const message: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit() + .create() as JetstreamEventCommit; console.log = jest.fn(); beforeEach(() => { @@ -77,7 +57,16 @@ describe('LogInputTextAction', () => { action = DebugLogAction.make('TEST', 'Hello'); - await action.handle(message, handlerAgent); + await action.handle(handlerAgent, message); + expect(console.log).toHaveBeenCalledWith(expected); + }); + + it('Should log info when no level given without make', async () => { + const expected = '1/31/2023, 07:00 PM | TEST | INFO | Hello'; + + action = new DebugLogAction('TEST', 'Hello'); + + await action.handle(handlerAgent, message); expect(console.log).toHaveBeenCalledWith(expected); }); @@ -86,7 +75,7 @@ describe('LogInputTextAction', () => { action = DebugLogAction.make('TEST', 'Hello', 'info'); - await action.handle(message, handlerAgent); + await action.handle(handlerAgent, message); expect(console.log).toHaveBeenCalledWith(expected); }); @@ -95,7 +84,7 @@ describe('LogInputTextAction', () => { action = DebugLogAction.make('TEST', 'Hello', 'warn'); - await action.handle(message, handlerAgent); + await action.handle(handlerAgent, message); expect(console.log).toHaveBeenCalledWith(expected); }); @@ -104,7 +93,7 @@ describe('LogInputTextAction', () => { action = DebugLogAction.make('TEST', 'Hello', 'error'); - await action.handle(message, handlerAgent); + await action.handle(handlerAgent, message); expect(console.log).toHaveBeenCalledWith(expected); }); }); diff --git a/tests/actions/TestAction.test.ts b/tests/actions/TestAction.test.ts index 22ce982..77333d0 100644 --- a/tests/actions/TestAction.test.ts +++ b/tests/actions/TestAction.test.ts @@ -1,22 +1,23 @@ import { HandlerAgent, - JetstreamMessage, - JetstreamMessageFactory, - LogMessageAction, + JetstreamEventCommit, + JetstreamEventFactory, TestAction, } from '../../src'; -import mocked = jest.mocked; import { advanceTo } from 'jest-date-mock'; +import mocked = jest.mocked; describe('TestAction', () => { let action: TestAction; let handlerAgent: HandlerAgent; - let message: JetstreamMessage; + let message: JetstreamEventCommit; console.log = jest.fn(); beforeEach(() => { handlerAgent = {} as HandlerAgent; - message = JetstreamMessageFactory.factory().create(); + message = JetstreamEventFactory.factory() + .commit() + .create() as JetstreamEventCommit; action = new TestAction(); advanceTo(new Date(Date.UTC(2023, 1, 1, 1, 0, 0))); mocked(process.env, { shallow: true }).DEBUG_LOG_ACTIVE = 'true'; @@ -28,7 +29,7 @@ describe('TestAction', () => { }); it('Should log Working', async () => { - await action.handle(message, handlerAgent); + await action.handle(handlerAgent); expect(console.log).toHaveBeenCalledWith( '1/31/2023, 07:00 PM | Working | INFO | working' ); diff --git a/tests/actions/TestMessageAction.test.ts b/tests/actions/TestMessageAction.test.ts new file mode 100644 index 0000000..254c533 --- /dev/null +++ b/tests/actions/TestMessageAction.test.ts @@ -0,0 +1,43 @@ +import { + HandlerAgent, + JetstreamEventCommit, + JetstreamEventFactory, + TestMessageAction, +} from '../../src'; +import { advanceTo } from 'jest-date-mock'; +import mocked = jest.mocked; + +describe('TestMessageAction', () => { + let action: TestMessageAction; + let handlerAgent: HandlerAgent; + let message: JetstreamEventCommit; + console.log = jest.fn(); + + beforeEach(() => { + handlerAgent = {} as HandlerAgent; + message = JetstreamEventFactory.factory() + .commit() + .create() as JetstreamEventCommit; + action = new TestMessageAction(); + advanceTo(new Date(Date.UTC(2023, 1, 1, 1, 0, 0))); + mocked(process.env, { shallow: true }).DEBUG_LOG_ACTIVE = 'true'; + mocked(process.env, { shallow: true }).DEBUG_LOG_LEVEL = 'info'; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should log Working', async () => { + await action.handle(handlerAgent, message); + expect(console.log).toHaveBeenCalledWith( + '1/31/2023, 07:00 PM | Working | INFO | working' + ); + }); + + it('Should Throw Error when running make', () => { + expect(TestMessageAction.make).toThrow( + 'Method not implemented! Use constructor!' + ); + }); +}); diff --git a/tests/actions/message/MessageLoggingActions.test.ts b/tests/actions/message/MessageLoggingActions.test.ts new file mode 100644 index 0000000..7933430 --- /dev/null +++ b/tests/actions/message/MessageLoggingActions.test.ts @@ -0,0 +1,30 @@ +import { + HandlerAgent, + JetstreamEventCommit, + JetstreamEventFactory, + LogMessageAction, +} from '../../../src'; + +describe('LogMessageAction', () => { + let action: LogMessageAction; + let handlerAgent: HandlerAgent; + let message: JetstreamEventCommit; + console.log = jest.fn(); + + beforeEach(() => { + handlerAgent = {} as HandlerAgent; + message = JetstreamEventFactory.factory() + .commit() + .create() as JetstreamEventCommit; + action = LogMessageAction.make(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should log output of RepoOp object when handle() is called', async () => { + await action.handle(handlerAgent, message); + expect(console.log).toHaveBeenCalledWith(message); + }); +}); diff --git a/tests/actions/post/SkeetActions/CreateSkeetAction.test.ts b/tests/actions/post/SkeetActions/CreateSkeetAction.test.ts index 1cc9e48..12c90d4 100644 --- a/tests/actions/post/SkeetActions/CreateSkeetAction.test.ts +++ b/tests/actions/post/SkeetActions/CreateSkeetAction.test.ts @@ -1,15 +1,15 @@ import { - CreateSkeetAction, + CreateSkeetMessageAction, CreateSkeetWithGeneratedTextAction, HandlerAgent, - JetstreamMessage, - JetstreamMessageFactory, + JetstreamEventCommit, + JetstreamEventFactory, } from '../../../../src'; describe('Create Skeet Action', () => { - let action: CreateSkeetAction; + let action: CreateSkeetMessageAction; let handlerAgent: HandlerAgent; - let message: JetstreamMessage; + let message: JetstreamEventCommit; const mockCreateSkeet = jest.fn(); const skeetText: string = 'Test Text'; @@ -17,8 +17,10 @@ describe('Create Skeet Action', () => { handlerAgent = { createSkeet: mockCreateSkeet, } as unknown as HandlerAgent; - message = JetstreamMessageFactory.factory().create(); - action = CreateSkeetAction.make(skeetText); + message = JetstreamEventFactory.factory() + .commit() + .create() as JetstreamEventCommit; + action = CreateSkeetMessageAction.make(skeetText); }); afterEach(() => { @@ -26,15 +28,15 @@ describe('Create Skeet Action', () => { }); it('Should call CreateSkeet with text', async () => { - await action.handle(message, handlerAgent); - expect(mockCreateSkeet).toHaveBeenCalledWith(skeetText); + await action.handle(handlerAgent, message); + expect(mockCreateSkeet).toHaveBeenCalledWith(skeetText, undefined); }); }); describe('Create Skeet from generated text Action', () => { let action: CreateSkeetWithGeneratedTextAction; let handlerAgent: HandlerAgent; - let message: JetstreamMessage; + let message: JetstreamEventCommit; const mockGenerateText = jest.fn().mockReturnValue('hello'); const mockCreateSkeet = jest.fn(); const skeetText: string = 'Test Text'; @@ -43,7 +45,9 @@ describe('Create Skeet from generated text Action', () => { handlerAgent = { createSkeet: mockCreateSkeet, } as unknown as HandlerAgent; - message = JetstreamMessageFactory.factory().create(); + message = JetstreamEventFactory.factory() + .commit() + .create() as JetstreamEventCommit; action = CreateSkeetWithGeneratedTextAction.make(mockGenerateText); }); @@ -52,8 +56,8 @@ describe('Create Skeet from generated text Action', () => { }); it('Should call CreateSkeet with text', async () => { - await action.handle(message, handlerAgent); - expect(mockGenerateText).toHaveBeenCalledWith(message, handlerAgent); + await action.handle(handlerAgent, message); + expect(mockGenerateText).toHaveBeenCalledWith(handlerAgent, message); expect(mockCreateSkeet).toHaveBeenCalledWith('hello'); }); }); diff --git a/tests/actions/post/SkeetActions/ReplyToSkeetAction.test.ts b/tests/actions/post/SkeetActions/ReplyToSkeetAction.test.ts index cb003ae..5aff1c3 100644 --- a/tests/actions/post/SkeetActions/ReplyToSkeetAction.test.ts +++ b/tests/actions/post/SkeetActions/ReplyToSkeetAction.test.ts @@ -1,30 +1,47 @@ import { - CreateSkeetMessage, - CreateSkeetMessageFactory, HandlerAgent, - Reply, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + JetstreamReply, + NewSkeetRecordFactory, ReplyFactory, ReplyToSkeetAction, ReplyToSkeetWithGeneratedTextAction, } from '../../../../src'; -describe('Reply To Skeet Action', () => { +describe('JetstreamReply To Skeet Action', () => { let action: ReplyToSkeetAction; let handlerAgent: HandlerAgent; - let message: CreateSkeetMessage; const mockCreateSkeet = jest.fn(); - const mockReply: Reply = ReplyFactory.factory().create(); + const mockReply: JetstreamReply = ReplyFactory.factory().create(); const mockGenerateReplyFromMessage = jest.fn().mockReturnValue(mockReply); const skeetText: string = 'Test Text'; const did: string = 'did:plc:did'; + const createMessage = (did: string) => { + return JetstreamEventFactory.factory() + .fromDid(did) + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record( + NewSkeetRecordFactory.factory() + + .create() + ) + .create() + ) + .create() as JetstreamEventCommit; + }; + beforeEach(() => { handlerAgent = { createSkeet: mockCreateSkeet, generateReplyFromMessage: mockGenerateReplyFromMessage, } as unknown as HandlerAgent; - message = CreateSkeetMessageFactory.factory().fromDid(did).create(); action = ReplyToSkeetAction.make(skeetText); }); @@ -33,29 +50,45 @@ describe('Reply To Skeet Action', () => { }); it('Should call CreateSkeet with text', async () => { - await action.handle(message, handlerAgent); + const message = createMessage(did); + await action.handle(handlerAgent, message); expect(mockCreateSkeet).toHaveBeenCalledWith(skeetText, mockReply); }); }); -describe('Reply To Skeet with generated text Action', () => { +describe('JetstreamReply To Skeet with generated text Action', () => { let action: ReplyToSkeetWithGeneratedTextAction; let handlerAgent: HandlerAgent; - let message: CreateSkeetMessage; const skeetText: string = 'Test Text'; const mockTextGenerator = jest.fn().mockReturnValue(skeetText); const mockCreateSkeet = jest.fn(); - const mockReply: Reply = ReplyFactory.factory().create(); + const mockReply: JetstreamReply = ReplyFactory.factory().create(); const mockGenerateReplyFromMessage = jest.fn().mockReturnValue(mockReply); const did: string = 'did:plc:did'; + const createMessage = (did: string) => { + return JetstreamEventFactory.factory() + .fromDid(did) + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record( + NewSkeetRecordFactory.factory() + + .create() + ) + .create() + ) + .create() as JetstreamEventCommit; + }; + beforeEach(() => { handlerAgent = { createSkeet: mockCreateSkeet, generateReplyFromMessage: mockGenerateReplyFromMessage, } as unknown as HandlerAgent; - message = CreateSkeetMessageFactory.factory().fromDid(did).create(); action = ReplyToSkeetWithGeneratedTextAction.make(mockTextGenerator); }); @@ -64,8 +97,9 @@ describe('Reply To Skeet with generated text Action', () => { }); it('Should call CreateSkeet with text', async () => { - await action.handle(message, handlerAgent); - expect(mockTextGenerator).toHaveBeenCalledWith(message, handlerAgent); + const message = createMessage(did); + await action.handle(handlerAgent, message); + expect(mockTextGenerator).toHaveBeenCalledWith(handlerAgent, message); expect(mockCreateSkeet).toHaveBeenCalledWith(skeetText, mockReply); }); }); diff --git a/tests/actions/standard-bsky-actions/FollowActions.test.ts b/tests/actions/standard-bsky-actions/FollowActions.test.ts new file mode 100644 index 0000000..b9601a1 --- /dev/null +++ b/tests/actions/standard-bsky-actions/FollowActions.test.ts @@ -0,0 +1,28 @@ +import { CreateFollowAction, DeleteFollowAction } from '../../../src'; +import { + runTestSuiteSingleParam, + TestCaseSingleParam, +} from './StandardTestSuite'; + +const testCases: TestCaseSingleParam[] = [ + { + description: 'Create Follow Action', + mockHandler: 'followUser', + actionFactory: CreateFollowAction.make, + staticValue: 'did:plc:static', + staticExpectation: 'did:plc:static', + dynamicGenerator: jest.fn().mockReturnValue('did:plc:dynamic'), + dynamicExpectation: 'did:plc:dynamic', + }, + { + description: 'Delete Follow Action', + mockHandler: 'unfollowUser', + actionFactory: DeleteFollowAction.make, + staticValue: 'did:plc:static', + staticExpectation: 'did:plc:static', + dynamicGenerator: jest.fn().mockReturnValue('did:plc:dynamic'), + dynamicExpectation: 'did:plc:dynamic', + }, +]; + +runTestSuiteSingleParam(testCases); diff --git a/tests/actions/standard-bsky-actions/LikeActions.test.ts b/tests/actions/standard-bsky-actions/LikeActions.test.ts new file mode 100644 index 0000000..fb37697 --- /dev/null +++ b/tests/actions/standard-bsky-actions/LikeActions.test.ts @@ -0,0 +1,37 @@ +import { CreateLikeAction, DeleteLikeAction } from '../../../src'; +import { + runTestSuiteDualParam, + runTestSuiteSingleParam, + TestCaseDualParam, + TestCaseSingleParam, +} from './StandardTestSuite'; + +const testCasesSingleParam: TestCaseSingleParam[] = [ + { + description: 'Delete like Action', + mockHandler: 'unlikeSkeet', + actionFactory: DeleteLikeAction.make, + staticValue: 'uri://static', + staticExpectation: 'uri://static', + dynamicGenerator: jest.fn().mockReturnValue('uri://dynamic'), + dynamicExpectation: 'uri://dynamic', + }, +]; + +const testCasesDualParam: TestCaseDualParam[] = [ + { + description: 'Create Like Action', + mockHandler: 'likeSkeet', + actionFactory: CreateLikeAction.make, + staticValues: ['uri://static', 'cid:static'], + staticExpectations: ['uri://static', 'cid:static'], + dynamicGenerators: [ + jest.fn().mockReturnValue('uri://dynamic'), + jest.fn().mockReturnValue('cid:dynamic'), + ], + dynamicExpectations: ['uri://dynamic', 'cid:dynamic'], + }, +]; + +runTestSuiteSingleParam(testCasesSingleParam); +runTestSuiteDualParam(testCasesDualParam); diff --git a/tests/actions/standard-bsky-actions/ReskeetActions.test.ts b/tests/actions/standard-bsky-actions/ReskeetActions.test.ts new file mode 100644 index 0000000..913f3c0 --- /dev/null +++ b/tests/actions/standard-bsky-actions/ReskeetActions.test.ts @@ -0,0 +1,37 @@ +import { CreateReskeetAction, DeleteReskeetAction } from '../../../src'; +import { + runTestSuiteDualParam, + runTestSuiteSingleParam, + TestCaseDualParam, + TestCaseSingleParam, +} from './StandardTestSuite'; + +const testCasesSingleParam: TestCaseSingleParam[] = [ + { + description: 'Delete Reskeet Action', + mockHandler: 'unreskeetSkeet', + actionFactory: DeleteReskeetAction.make, + staticValue: 'uri://static', + staticExpectation: 'uri://static', + dynamicGenerator: jest.fn().mockReturnValue('uri://dynamic'), + dynamicExpectation: 'uri://dynamic', + }, +]; + +const testCasesDualParam: TestCaseDualParam[] = [ + { + description: 'Create Reskeet Action', + mockHandler: 'reskeetSkeet', + actionFactory: CreateReskeetAction.make, + staticValues: ['uri://static', 'cid:static'], + staticExpectations: ['uri://static', 'cid:static'], + dynamicGenerators: [ + jest.fn().mockReturnValue('uri://dynamic'), + jest.fn().mockReturnValue('cid:dynamic'), + ], + dynamicExpectations: ['uri://dynamic', 'cid:dynamic'], + }, +]; + +runTestSuiteSingleParam(testCasesSingleParam); +runTestSuiteDualParam(testCasesDualParam); diff --git a/tests/actions/standard-bsky-actions/SkeetActions.test.ts b/tests/actions/standard-bsky-actions/SkeetActions.test.ts new file mode 100644 index 0000000..2785a91 --- /dev/null +++ b/tests/actions/standard-bsky-actions/SkeetActions.test.ts @@ -0,0 +1,45 @@ +import { + CreateSkeetAction, + DeleteSkeetAction, + ReplyFactory, +} from '../../../src'; +import { + runTestSuiteDualParam, + runTestSuiteSingleParam, + TestCaseDualParam, + TestCaseSingleParam, +} from './StandardTestSuite'; + +const testCases: TestCaseSingleParam[] = [ + { + description: 'Delete Skeet Action', + mockHandler: 'deleteSkeet', + actionFactory: DeleteSkeetAction.make, + staticValue: 'uri://string', + staticExpectation: 'uri://string', + dynamicGenerator: jest.fn().mockReturnValue('uri://generated'), + dynamicExpectation: 'uri://generated', + }, +]; + +// @ts-ignore +const testCasesDualParam: TestCaseDualParam[] = [ + { + description: 'Create Skeet Action', + mockHandler: 'createSkeet', + actionFactory: CreateSkeetAction.make, + staticValues: ['Test Text', ReplyFactory.factory().create()], + staticExpectations: ['Test Text', ReplyFactory.factory().create()], + dynamicGenerators: [ + jest.fn().mockReturnValue('Generated Text'), + jest.fn().mockReturnValue(ReplyFactory.factory().create()), + ], + dynamicExpectations: [ + 'Generated Text', + ReplyFactory.factory().create(), + ], + }, +]; + +runTestSuiteSingleParam(testCases); +runTestSuiteDualParam(testCasesDualParam); diff --git a/tests/actions/standard-bsky-actions/StandardTestSuite.ts b/tests/actions/standard-bsky-actions/StandardTestSuite.ts new file mode 100644 index 0000000..da87f3a --- /dev/null +++ b/tests/actions/standard-bsky-actions/StandardTestSuite.ts @@ -0,0 +1,93 @@ +// testSuite.ts +import { HandlerAgent } from '../../../src'; + +export interface TestCaseSingleParam { + description: string; + mockHandler: string; + actionFactory: any; + staticValue: string; + staticExpectation: string; + dynamicGenerator: any; + dynamicExpectation: string; +} + +export const runTestSuiteSingleParam = (testCases: TestCaseSingleParam[]) => { + describe.each(testCases)( + '$description', + (testCase: TestCaseSingleParam) => { + let action; + let handlerAgent: HandlerAgent; + const mockHandler = jest.fn(); + + beforeEach(() => { + handlerAgent = { + [testCase.mockHandler]: mockHandler, + } as unknown as HandlerAgent; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should call with static value', async () => { + action = testCase.actionFactory(testCase.staticValue); + await action.handle(handlerAgent); + expect(mockHandler).toHaveBeenCalledWith( + testCase.staticExpectation + ); + }); + + it('Should call with value from generator', async () => { + action = testCase.actionFactory(testCase.dynamicGenerator); + await action.handle(handlerAgent); + expect(mockHandler).toHaveBeenCalledWith( + testCase.dynamicExpectation + ); + }); + } + ); +}; + +export interface TestCaseDualParam { + description: string; + mockHandler: string; + actionFactory: any; + staticValues: [any, any]; + staticExpectations: [any, any]; + dynamicGenerators: [any, any]; + dynamicExpectations: [any, any]; +} + +export const runTestSuiteDualParam = (testCases: TestCaseDualParam[]) => { + describe.each(testCases)('$description', (testCase: TestCaseDualParam) => { + let action; + let handlerAgent: HandlerAgent; + const mockHandler = jest.fn(); + + beforeEach(() => { + handlerAgent = { + [testCase.mockHandler]: mockHandler, + } as unknown as HandlerAgent; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should call with static values', async () => { + action = testCase.actionFactory(...testCase.staticValues); + await action.handle(handlerAgent); + expect(mockHandler).toHaveBeenCalledWith( + ...testCase.staticExpectations + ); + }); + + it('Should call with values from generators', async () => { + action = testCase.actionFactory(...testCase.dynamicGenerators); + await action.handle(handlerAgent); + expect(mockHandler).toHaveBeenCalledWith( + ...testCase.dynamicExpectations + ); + }); + }); +}; diff --git a/tests/agent/HandlerAgentFollow.test.ts b/tests/agent/HandlerAgentFollow.test.ts index 04f575f..1364f27 100644 --- a/tests/agent/HandlerAgentFollow.test.ts +++ b/tests/agent/HandlerAgentFollow.test.ts @@ -1,7 +1,6 @@ import dotenv from 'dotenv'; import { HandlerAgent } from '../../src'; -import atprotoApiMock, { AtpSessionData, BskyAgent } from '@atproto/api'; -import clearAllMocks = jest.clearAllMocks; +import { BskyAgent } from '@atproto/api'; dotenv.config(); @@ -10,42 +9,45 @@ describe('HandlerAgent', () => { const testHandle: string = 'testhandle'; const testPassword: string = 'testpassword'; - const followingMocks = [ - { - did: 'isFollowing', - viewer: { - following: 'followLink', + const followingMocks = { + follows: [ + { + did: 'isFollowing', + viewer: { + following: 'followLink', + }, }, - }, - ]; - - const followedByMocks = [ - { - did: 'isFollowedBy', - viewer: { - following: 'followLink', - followedBy: 'followedByLink', + ], + }; + + const followedByMocks = { + followers: [ + { + did: 'isFollowedBy', + viewer: { + following: 'followLink', + followedBy: 'followedByLink', + }, }, - }, - ]; + ], + }; let getFollowsMock: jest.Mock; let getFollowersMock: jest.Mock; const followMock = jest.fn(); const deleteFollowMock = jest.fn(); + let getProfileMock: jest.Mock; beforeEach(() => { jest.clearAllMocks(); - getFollowsMock = jest - .fn() - .mockReturnValue({ data: { follows: followingMocks } }); - getFollowersMock = jest - .fn() - .mockReturnValue({ data: { followers: followedByMocks } }); + getFollowsMock = jest.fn().mockReturnValue({ data: followingMocks }); + getFollowersMock = jest.fn().mockReturnValue({ data: followedByMocks }); + // Require mocked module and define class' methods const mockedAgent = { getFollows: getFollowsMock, getFollowers: getFollowersMock, follow: followMock, deleteFollow: deleteFollowMock, + getProfile: getProfileMock, } as unknown as BskyAgent; handlerAgent = new HandlerAgent( 'agentName', @@ -67,15 +69,45 @@ describe('HandlerAgent', () => { expect(followers).toEqual(followedByMocks); }); - it('IsFollowing should call agent getFollows and return true if following', async () => { + it('IsFollowing should call agent getProfile and return true if following', async () => { + getProfileMock = jest + .fn() + .mockReturnValue({ data: { viewer: { following: 'uri' } } }); + const mockedAgent = { + getFollows: getFollowsMock, + getFollowers: getFollowersMock, + follow: followMock, + deleteFollow: deleteFollowMock, + getProfile: getProfileMock, + } as unknown as BskyAgent; + handlerAgent = new HandlerAgent( + 'agentName', + testHandle, + testPassword, + mockedAgent + ); const isFollowing = await handlerAgent.isFollowing('isFollowing'); - expect(getFollowsMock).toHaveBeenCalled(); + expect(getProfileMock).toHaveBeenCalled(); expect(isFollowing).toBe(true); }); - it('IsFollowing should call agent getFollows and return false if not following', async () => { + it('IsFollowing should call agent getProfile and return false if not following', async () => { + getProfileMock = jest.fn().mockReturnValue({ data: { viewer: {} } }); + const mockedAgent = { + getFollows: getFollowsMock, + getFollowers: getFollowersMock, + follow: followMock, + deleteFollow: deleteFollowMock, + getProfile: getProfileMock, + } as unknown as BskyAgent; + handlerAgent = new HandlerAgent( + 'agentName', + testHandle, + testPassword, + mockedAgent + ); const isFollowing = await handlerAgent.isFollowing('badDid'); - expect(getFollowsMock).toHaveBeenCalled(); + expect(getProfileMock).toHaveBeenCalled(); expect(isFollowing).toBe(false); }); @@ -83,11 +115,13 @@ describe('HandlerAgent', () => { getFollowsMock = jest .fn() .mockReturnValue({ data: { follows: undefined } }); + getProfileMock = jest.fn().mockReturnValue({ data: undefined }); const mockedAgent = { getFollows: getFollowsMock, getFollowers: getFollowersMock, follow: followMock, deleteFollow: deleteFollowMock, + getProfile: getProfileMock, } as unknown as BskyAgent; handlerAgent = new HandlerAgent( 'agentName', @@ -96,19 +130,49 @@ describe('HandlerAgent', () => { mockedAgent ); const isFollowing = await handlerAgent.isFollowing('badDid'); - expect(getFollowsMock).toHaveBeenCalled(); + expect(getProfileMock).toHaveBeenCalled(); expect(isFollowing).toBe(false); }); it('IsFollowedBy should call agent getFollowers and return true if followed by', async () => { + getProfileMock = jest + .fn() + .mockReturnValue({ data: { viewer: { followedBy: 'uri' } } }); + const mockedAgent = { + getFollows: getFollowsMock, + getFollowers: getFollowersMock, + follow: followMock, + deleteFollow: deleteFollowMock, + getProfile: getProfileMock, + } as unknown as BskyAgent; + handlerAgent = new HandlerAgent( + 'agentName', + testHandle, + testPassword, + mockedAgent + ); const isFollowedBy = await handlerAgent.isFollowedBy('isFollowedBy'); - expect(getFollowersMock).toHaveBeenCalled(); + expect(getProfileMock).toHaveBeenCalled(); expect(isFollowedBy).toBe(true); }); it('IsFollowedBy should call agent getFollowers and return false if not followed by', async () => { + getProfileMock = jest.fn().mockReturnValue({ data: { viewer: {} } }); + const mockedAgent = { + getFollows: getFollowsMock, + getFollowers: getFollowersMock, + follow: followMock, + deleteFollow: deleteFollowMock, + getProfile: getProfileMock, + } as unknown as BskyAgent; + handlerAgent = new HandlerAgent( + 'agentName', + testHandle, + testPassword, + mockedAgent + ); const isFollowedBy = await handlerAgent.isFollowedBy('badDid'); - expect(getFollowersMock).toHaveBeenCalled(); + expect(getProfileMock).toHaveBeenCalled(); expect(isFollowedBy).toBe(false); }); @@ -116,11 +180,13 @@ describe('HandlerAgent', () => { getFollowersMock = jest .fn() .mockReturnValue({ data: { followers: undefined } }); + getProfileMock = jest.fn().mockReturnValue({ data: undefined }); const mockedAgent = { getFollows: getFollowsMock, getFollowers: getFollowersMock, follow: followMock, deleteFollow: deleteFollowMock, + getProfile: getProfileMock, } as unknown as BskyAgent; handlerAgent = new HandlerAgent( 'agentName', @@ -129,7 +195,7 @@ describe('HandlerAgent', () => { mockedAgent ); const isFollowedBy = await handlerAgent.isFollowedBy('badDid'); - expect(getFollowersMock).toHaveBeenCalled(); + expect(getProfileMock).toHaveBeenCalled(); expect(isFollowedBy).toBe(false); }); @@ -140,23 +206,37 @@ describe('HandlerAgent', () => { }); it('deleteFollow should call mock deleteFollow and extract correct url', async () => { + getProfileMock = jest + .fn() + .mockReturnValue({ data: { viewer: { following: 'uri' } } }); + const mockedAgent = { + getFollows: getFollowsMock, + getFollowers: getFollowersMock, + follow: followMock, + deleteFollow: deleteFollowMock, + getProfile: getProfileMock, + } as unknown as BskyAgent; + handlerAgent = new HandlerAgent( + 'agentName', + testHandle, + testPassword, + mockedAgent + ); const did = 'isFollowing'; await handlerAgent.unfollowUser(did); - expect(deleteFollowMock).toHaveBeenCalledWith('followLink'); + expect(getProfileMock).toHaveBeenCalledWith({ actor: did }); + expect(deleteFollowMock).toHaveBeenCalledWith('uri'); }); - it('deleteFollow should return false when getFollows is undefined', async () => { + it('deleteFollow should return false when no profile', async () => { const followsRespMock = undefined; - getFollowsMock = jest.fn().mockReturnValue({ - data: { - follows: followsRespMock, - }, - }); + getProfileMock = jest.fn().mockReturnValue({ data: undefined }); const mockedAgent = { getFollows: getFollowsMock, getFollowers: getFollowersMock, follow: followMock, deleteFollow: deleteFollowMock, + getProfile: getProfileMock, } as unknown as BskyAgent; handlerAgent = new HandlerAgent( 'agentName', @@ -165,35 +245,23 @@ describe('HandlerAgent', () => { mockedAgent ); - const mockGetRecordForDid = jest.fn().mockReturnValue({}); - handlerAgent.getRecordForDid = mockGetRecordForDid; const did = 'isFollowing'; const resp = await handlerAgent.unfollowUser(did); - expect(mockGetRecordForDid).not.toHaveBeenCalled(); + expect(getProfileMock).toHaveBeenCalledWith({ actor: did }); + expect(deleteFollowMock).not.toHaveBeenCalled(); expect(resp).toBe(false); }); - it('deleteFollow should return false when getFollows is undefined', async () => { - const followsRespMock = [ - { - did: 'isFollowing', - viewer: { - following: undefined, - followedBy: undefined, - }, - }, - ]; - getFollowsMock = jest.fn().mockReturnValue({ - data: { - follows: followsRespMock, - }, - }); + it('deleteFollow should return false when no profile', async () => { + const followsRespMock = undefined; + getProfileMock = jest.fn().mockReturnValue({ data: { viewer: {} } }); const mockedAgent = { getFollows: getFollowsMock, getFollowers: getFollowersMock, follow: followMock, deleteFollow: deleteFollowMock, + getProfile: getProfileMock, } as unknown as BskyAgent; handlerAgent = new HandlerAgent( 'agentName', @@ -202,11 +270,10 @@ describe('HandlerAgent', () => { mockedAgent ); - const mockGetRecordForDid = jest.fn().mockReturnValue({}); - handlerAgent.getRecordForDid = mockGetRecordForDid; const did = 'isFollowing'; const resp = await handlerAgent.unfollowUser(did); - expect(mockGetRecordForDid).toHaveBeenCalledWith(did, followsRespMock); + expect(getProfileMock).toHaveBeenCalledWith({ actor: did }); + expect(deleteFollowMock).not.toHaveBeenCalled(); expect(resp).toBe(false); }); diff --git a/tests/agent/HandlerAgentGetterSetter.test.ts b/tests/agent/HandlerAgentGetterSetter.test.ts index f5a38e0..067f0d5 100644 --- a/tests/agent/HandlerAgentGetterSetter.test.ts +++ b/tests/agent/HandlerAgentGetterSetter.test.ts @@ -1,12 +1,22 @@ import { HandlerAgent } from '../../src'; import { AtpSessionData, BskyAgent } from '@atproto/api'; import dotenv from 'dotenv'; +import fs from 'fs'; dotenv.config(); +process.env.SESSION_DATA_PATH = './tests/temp/getterSetter'; jest.mock('@atproto/api', () => jest.genMockFromModule('@atproto/api')); describe('HandlerAgent', () => { + afterAll(() => { + fs.rmSync('./tests/temp/getterSetter', { + recursive: true, + force: true, + }); + }); + fs.mkdirSync('./tests/temp/getterSetter', { recursive: true }); + let handlerAgent: HandlerAgent; const testHandle: string = 'testhandle'; const testPassword: string = 'testpassword'; @@ -25,6 +35,10 @@ describe('HandlerAgent', () => { mockedAgent ); } + + if (fs.existsSync('./agentName-session.json')) { + fs.unlinkSync('./agentName-session.json'); + } }); it('#getAgent & setAgent should get correct agent value', () => { @@ -45,6 +59,9 @@ describe('HandlerAgent', () => { const testAgentName = 'TestAgent'; handlerAgent.setAgentName = testAgentName; expect(handlerAgent.getAgentName).toBe(testAgentName); + if (fs.existsSync('./Test Agent-session.json')) { + fs.unlinkSync('./Test Agent-session.json'); + } }); it('#getHandle & setHandle should set correct handle value', () => { @@ -63,13 +80,20 @@ describe('HandlerAgent', () => { }); it('#getSession & setSession should set correct session value', () => { + handlerAgent.setAgentName = 'agentName'; + const saveSessionMock = jest.fn(); + handlerAgent.saveSessionData = saveSessionMock; const testSession = { did: 'did:plc:2bnsooklzchcu5ao7xdjosrs', } as AtpSessionData; handlerAgent.setSession = testSession; + + expect(saveSessionMock).toBeCalledWith(testSession); + expect(handlerAgent.getSession).toBe(testSession); handlerAgent.setSession = undefined; expect(handlerAgent.getSession).toBe(false); + expect(saveSessionMock).toBeCalledTimes(1); }); }); diff --git a/tests/agent/HandlerAgentInitialization.test.ts b/tests/agent/HandlerAgentInitialization.test.ts index a3c9dcb..a8a9e56 100644 --- a/tests/agent/HandlerAgentInitialization.test.ts +++ b/tests/agent/HandlerAgentInitialization.test.ts @@ -1,16 +1,30 @@ import dotenv from 'dotenv'; import { HandlerAgent } from '../../src'; -import atprotoApiMock, { AtpSessionData, BskyAgent } from '@atproto/api'; +import { AtpSessionData, BskyAgent } from '@atproto/api'; +import fs from 'fs'; dotenv.config(); +process.env.SESSION_DATA_PATH = './tests/temp/initialization'; describe('HandlerAgent', () => { + afterAll(() => { + fs.rmSync('./tests/temp/initialization', { + recursive: true, + force: true, + }); + }); + fs.mkdirSync('./tests/temp/initialization', { recursive: true }); let handlerAgent: HandlerAgent; const testHandle: string | undefined = process.env.TEST_HANDLE ?? 'testhandle'; const testPassword: string | undefined = process.env.TEST_PASSWORD ?? 'testpassword'; - const loginMock = jest.fn(); + const loginMock = jest.fn(() => { + handlerAgent.setSession = { + did: 'did:plc:2bnsooklzchcu5ao7xdjosrs', + // add any other session values needed for your tests + } as AtpSessionData; + }); const resumeSessionMock = jest.fn(); beforeEach(() => { if (testHandle !== undefined && testPassword !== undefined) { @@ -35,18 +49,16 @@ describe('HandlerAgent', () => { }); it('authenticate() should login and resume session if agent exists', async () => { - // Manually set the session - handlerAgent.setSession = { - did: 'did:plc:2bnsooklzchcu5ao7xdjosrs', - // add any other session values needed for your tests - } as AtpSessionData; - handlerAgent.setDid = 'did:plc:2bnsooklzchcu5ao7xdjosrs'; + // Simulate no existing session + handlerAgent.setSession = undefined; + handlerAgent.setDid = undefined; + await handlerAgent.authenticate(); expect(loginMock).toHaveBeenCalledTimes(1); expect(loginMock).toHaveBeenCalledWith({ identifier: testHandle, password: testPassword, }); - expect(handlerAgent.getDid).toBe('did:plc:2bnsooklzchcu5ao7xdjosrs'); + expect(handlerAgent.getDid).toBeDefined(); }); }); diff --git a/tests/agent/HandlerAgentInitializeAgent.test.ts b/tests/agent/HandlerAgentInitializeAgent.test.ts index 45b0044..f8097ca 100644 --- a/tests/agent/HandlerAgentInitializeAgent.test.ts +++ b/tests/agent/HandlerAgentInitializeAgent.test.ts @@ -1,5 +1,10 @@ import { HandlerAgent } from '../../src'; import { BskyAgent } from '@atproto/api'; +import fs from 'fs'; +import dotenv from 'dotenv'; + +dotenv.config(); +process.env.SESSION_DATA_PATH = './tests/temp'; describe('HandlerAgent', () => { it('should initialize BskyAgent if agent is not provided', () => { @@ -24,6 +29,10 @@ describe('HandlerAgent', () => { // Clean up by removing the spy initializeBskyAgentSpy.mockRestore(); + + if (fs.existsSync(handlerAgent.getSessionLocation())) { + fs.unlinkSync(handlerAgent.getSessionLocation()); + } }); it('should initialize BskyAgent object if agent is null', async () => { @@ -65,6 +74,10 @@ describe('HandlerAgent', () => { // Clean up by removing the spy initializeBskyAgentSpy.mockRestore(); + + if (fs.existsSync(handlerAgent.getSessionLocation())) { + fs.unlinkSync(handlerAgent.getSessionLocation()); + } }); it('should throw error when session is undefined', async () => { @@ -75,6 +88,10 @@ describe('HandlerAgent', () => { 'Test Password' ); + if (fs.existsSync(handlerAgent.getSessionLocation())) { + fs.unlinkSync(handlerAgent.getSessionLocation()); + } + // Set session to null handlerAgent.setSession = undefined; @@ -85,6 +102,10 @@ describe('HandlerAgent', () => { await expect(handlerAgent.authenticate()).rejects.toThrow( 'Could not retrieve bluesky session data for reply bot' ); + + if (fs.existsSync(handlerAgent.getSessionLocation())) { + fs.unlinkSync(handlerAgent.getSessionLocation()); + } }); it('should throw error when agent is undefined after resumeSession', async () => { @@ -115,5 +136,9 @@ describe('HandlerAgent', () => { await expect(handlerAgent.authenticate()).rejects.toThrow( `Could not get agent from ${handlerAgent.getAgentName}` ); + + if (fs.existsSync(handlerAgent.getSessionLocation())) { + fs.unlinkSync(handlerAgent.getSessionLocation()); + } }); }); diff --git a/tests/agent/HandlerAgentPostCounts.test.ts b/tests/agent/HandlerAgentPostCounts.test.ts new file mode 100644 index 0000000..71b1d9c --- /dev/null +++ b/tests/agent/HandlerAgentPostCounts.test.ts @@ -0,0 +1,73 @@ +import dotenv from 'dotenv'; +import { HandlerAgent } from '../../src'; +import { BskyAgent } from '@atproto/api'; + +dotenv.config(); + +describe('HandlerAgent Post Count', () => { + let handlerAgent: HandlerAgent; + const testHandle: string = 'testhandle'; + const testPassword: string = 'testpassword'; + + // Mock data to be returned by the agent.getPostThread function + const mockPostThreadResponse = { + data: { + thread: { + post: { + uri: 'at://did:plc:example/app.bsky.feed.post/rkey', + cid: 'examplecid', + replyCount: 1, + repostCount: 0, + likeCount: 2, + quoteCount: 0, + }, + }, + }, + headers: { + header: '*', + }, + success: true, + }; + + let getPostThreadMock: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + getPostThreadMock = jest.fn().mockResolvedValue(mockPostThreadResponse); + + const mockedAgent = { + getPostThread: getPostThreadMock, + } as unknown as BskyAgent; + + handlerAgent = new HandlerAgent( + 'agentName', + testHandle, + testPassword, + mockedAgent + ); + }); + + it('should return correct like count for a post', async () => { + const likeCount = await handlerAgent.getPostLikeCount('someUri'); + expect(getPostThreadMock).toHaveBeenCalledWith({ uri: 'someUri' }); + expect(likeCount).toBe(2); + }); + + it('should return correct repost count for a post', async () => { + const repostCount = await handlerAgent.getPostRepostCount('someUri'); + expect(getPostThreadMock).toHaveBeenCalledWith({ uri: 'someUri' }); + expect(repostCount).toBe(0); + }); + + it('should return correct reply count for a post', async () => { + const replyCount = await handlerAgent.getPostReplyCount('someUri'); + expect(getPostThreadMock).toHaveBeenCalledWith({ uri: 'someUri' }); + expect(replyCount).toBe(1); + }); + + it('should return correct quote count for a post', async () => { + const quoteCount = await handlerAgent.getPostQuoteCount('someUri'); + expect(getPostThreadMock).toHaveBeenCalledWith({ uri: 'someUri' }); + expect(quoteCount).toBe(0); + }); +}); diff --git a/tests/agent/HandlerAgentSaveLoadSession.test.ts b/tests/agent/HandlerAgentSaveLoadSession.test.ts new file mode 100644 index 0000000..6de2660 --- /dev/null +++ b/tests/agent/HandlerAgentSaveLoadSession.test.ts @@ -0,0 +1,144 @@ +import dotenv from 'dotenv'; +import fs from 'fs'; +import { DebugLog, HandlerAgent } from '../../src'; +import { AtpSessionData } from '@atproto/api'; + +dotenv.config(); +process.env.SESSION_DATA_PATH = './tests/temp/saveLoad'; + +describe('HandlerAgent Session Management', () => { + fs.mkdirSync('./tests/temp/saveLoad', { recursive: true }); + let handlerAgent: HandlerAgent; + const testHandle: string | undefined = + process.env.TEST_HANDLE ?? 'testhandle'; + const testPassword: string | undefined = + process.env.TEST_PASSWORD ?? 'testpassword'; + + const sessionData: AtpSessionData = { + did: 'did:plc:2bnsooklzchcu5ao7xdjosrs', + // Add any other necessary fields for AtpSessionData + } as AtpSessionData; + const malformedSessionData = '{ malformed JSON }'; + const sessionFilePath = './agentName-session.json'; + + beforeEach(() => { + if (testHandle !== undefined && testPassword !== undefined) { + handlerAgent = new HandlerAgent( + 'agentName', + testHandle, + testPassword + ); + } + + // Clear any existing session files before each test + if (fs.existsSync(handlerAgent.getSessionLocation())) { + fs.unlinkSync(handlerAgent.getSessionLocation()); + } + }); + + afterEach(() => { + // Clean up any session files after each test + if (fs.existsSync(handlerAgent.getSessionLocation())) { + fs.unlinkSync(handlerAgent.getSessionLocation()); + } + }); + afterAll(() => { + fs.rmSync('./tests/temp/saveLoad', { recursive: true, force: true }); + }); + + it('should load existing session data if the session file exists', async () => { + fs.writeFileSync( + handlerAgent.getSessionLocation(), + JSON.stringify(sessionData), + 'utf8' + ); + + const debugLogSpy = jest.spyOn(DebugLog, 'warn'); + const resumeMock = jest.fn(); + const loginMock = jest.fn(); + // @ts-ignore + handlerAgent.agent.resumeSession = resumeMock; + // @ts-ignore + handlerAgent.agent.login = loginMock; + await handlerAgent.authenticate(); + + expect(debugLogSpy).toHaveBeenCalledWith( + 'AGENT', + 'Existing session. Loading session' + ); + expect(handlerAgent['session']).toEqual(sessionData); // Access private properties using bracket notation + debugLogSpy.mockRestore(); + expect(resumeMock).toBeCalledWith(handlerAgent.getSession); + }); + + it('should log an error and resolve undefined if session data parsing fails', async () => { + fs.writeFileSync( + handlerAgent.getSessionLocation(), + malformedSessionData, + 'utf8' + ); + + const debugLogSpy = jest.spyOn(DebugLog, 'error'); + + const loadedSession = await handlerAgent.loadSessionData(); + + expect(debugLogSpy).toHaveBeenCalledWith( + 'AGENT', + expect.stringContaining('Failed to parse session data.') + ); + expect(loadedSession).toBeUndefined(); + + debugLogSpy.mockRestore(); + }); + + it('should save session data', async () => { + await handlerAgent.saveSessionData(sessionData); + const savedData = JSON.parse( + fs.readFileSync(handlerAgent.getSessionLocation(), 'utf8') + ); + expect(savedData).toEqual(sessionData); + }); + + it('should return undefined if the session file does not exist', async () => { + const loadedSession = await handlerAgent.loadSessionData(); + expect(loadedSession).toBeUndefined(); + }); + + it('should load session data when file exists', async () => { + fs.writeFileSync( + handlerAgent.getSessionLocation(), + JSON.stringify(sessionData), + 'utf8' + ); + const loadedSession = await handlerAgent.loadSessionData(); + expect(loadedSession).toEqual(sessionData); + }); + + it('should handle error when saving session data if writing fails', async () => { + jest.spyOn(fs, 'writeFile').mockImplementationOnce( + (_, __, callback) => { + callback(new Error('Failed to save')); + } + ); + + await expect(handlerAgent.saveSessionData(sessionData)).rejects.toThrow( + 'Failed to save' + ); + }); + + it('should handle error when loading session data if reading fails', async () => { + fs.writeFileSync( + handlerAgent.getSessionLocation(), + JSON.stringify(sessionData), + 'utf8' + ); + + // @ts-ignore + jest.spyOn(fs, 'readFile').mockImplementationOnce((_, __, callback) => { + callback(new Error('Failed to read'), null as unknown as Buffer); + }); + + const loadedSession = await handlerAgent.loadSessionData(); + expect(loadedSession).toBeUndefined(); + }); +}); diff --git a/tests/agent/HandlerAgentSkeet.test.ts b/tests/agent/HandlerAgentSkeet.test.ts index d92898e..cae149e 100644 --- a/tests/agent/HandlerAgentSkeet.test.ts +++ b/tests/agent/HandlerAgentSkeet.test.ts @@ -1,10 +1,11 @@ import dotenv from 'dotenv'; import { + DebugLog, HandlerAgent, - Reply, + JetstreamReply, + JetstreamSubject, + JetstreamSubjectFactory, ReplyFactory, - Subject, - SubjectFactory, } from '../../src'; import { BskyAgent } from '@atproto/api'; @@ -22,7 +23,22 @@ describe('HandlerAgent', () => { const deleteLikeMock = jest.fn(); const repostMock = jest.fn(); const deleteRepostMock = jest.fn(); + const mockRecord = { + value: { + subject: { + uri: 'skeetURI', + }, + }, + uri: 'recordURI', + }; + const listRecordsMock = jest.fn().mockReturnValue({ + data: { + cursor: 'text', + records: [mockRecord], + }, + }); beforeEach(() => { + jest.clearAllMocks(); if (testHandle !== undefined && testPassword !== undefined) { // Require mocked module and define class' methods const mockedAgent = { @@ -32,6 +48,15 @@ describe('HandlerAgent', () => { deleteLike: deleteLikeMock, repost: repostMock, deleteRepost: deleteRepostMock, + api: { + com: { + atproto: { + repo: { + listRecords: listRecordsMock, + }, + }, + }, + }, } as unknown as BskyAgent; handlerAgent = new HandlerAgent( 'agentName', @@ -48,23 +73,23 @@ describe('HandlerAgent', () => { reply: undefined, }; await handlerAgent.post(input); - expect(postMock).toBeCalledWith(input); + expect(postMock).toHaveBeenCalledWith(input); }); describe('CreateSkeet', () => { it('createSkeet should call post with input text and no reply if no skeetReply', async () => { await handlerAgent.createSkeet('Test post'); - expect(postMock).toBeCalledWith({ text: 'Test post' }); + expect(postMock).toHaveBeenCalledWith({ text: 'Test post' }); }); it('createSkeet should call post with input text and reply if existingPostDetails is present', async () => { - const subject: Subject = SubjectFactory.make(); - const reply: Reply = ReplyFactory.factory() + const subject: JetstreamSubject = JetstreamSubjectFactory.make(); + const reply: JetstreamReply = ReplyFactory.factory() .parent(subject) .root(subject) .create(); await handlerAgent.createSkeet('Test post', reply); - expect(postMock).toBeCalledWith({ + expect(postMock).toHaveBeenCalledWith({ text: 'Test post', reply: reply, }); @@ -73,30 +98,171 @@ describe('HandlerAgent', () => { it('DeleteSkeet should call deletePost for given post', async () => { await handlerAgent.deleteSkeet('skeetURI'); - expect(deletePostMock).toBeCalledWith('skeetURI'); + expect(deletePostMock).toHaveBeenCalledWith('skeetURI'); }); describe('Like', () => { it('likeSkeet should call like for given post', async () => { await handlerAgent.likeSkeet('skeetURI', 'skeetCID'); - expect(likeMock).toBeCalledWith('skeetURI', 'skeetCID'); + expect(likeMock).toHaveBeenCalledWith('skeetURI', 'skeetCID'); }); it('unlikeSkeet should call deleteLike for given post', async () => { - await handlerAgent.unlikeSkeet('likeURI'); - expect(deleteLikeMock).toBeCalledWith('likeURI'); + await handlerAgent.unlikeSkeet('skeetURI'); + expect(deleteLikeMock).toHaveBeenCalledWith('recordURI'); + }); + + it('unlikeSkeet should not call deleteLike for given post', async () => { + await handlerAgent.unlikeSkeet('nonSkeet'); + expect(deleteLikeMock).not.toHaveBeenCalled(); }); }); describe('Reskeet', () => { it('reskeet should call repost for given post', async () => { await handlerAgent.reskeetSkeet('skeetURI', 'skeetCID'); - expect(repostMock).toBeCalledWith('skeetURI', 'skeetCID'); + expect(repostMock).toHaveBeenCalledWith('skeetURI', 'skeetCID'); }); it('unreskeet should call deleteRepost for given post', async () => { - await handlerAgent.unreskeetSkeet('reskeetURI'); - expect(deleteRepostMock).toBeCalledWith('reskeetURI'); + // TODO update this test to mock the listRecords function + await handlerAgent.unreskeetSkeet('skeetURI'); + expect(deleteRepostMock).toHaveBeenCalledWith('recordURI'); + }); + + it('unreskeet should not call deleteRepost for given post', async () => { + // TODO update this test to mock the listRecords function + await handlerAgent.unreskeetSkeet('nonSkeet'); + expect(deleteRepostMock).not.toHaveBeenCalled(); + }); + + it('unreskeet should not call deleteRepost for given post', async () => { + listRecordsMock.mockReturnValue(undefined); + const mockDebugError = jest.fn(); + DebugLog.error = mockDebugError; + // TODO update this test to mock the listRecords function + await handlerAgent.unreskeetSkeet('nonSkeet'); + expect(deleteRepostMock).not.toHaveBeenCalled(); + expect(mockDebugError).toHaveBeenCalledWith( + 'Handler Agent', + 'Failed to retrieve repost records' + ); + }); + }); +}); + +describe('HandlerAgent', () => { + let handlerAgent: HandlerAgent; + const testHandle: string | undefined = + process.env.TEST_HANDLE ?? 'testhandle'; + const testPassword: string | undefined = + process.env.TEST_PASSWORD ?? 'testpassword'; + const postMock = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + if (testHandle !== undefined && testPassword !== undefined) { + // Require mocked module and define class' methods + handlerAgent = new HandlerAgent( + 'agentName', + testHandle, + testPassword, + undefined + ); + } + + // @ts-ignore + handlerAgent.getAgent.post = postMock; + }); + + it('post should call post with input', async () => { + const input = { + text: 'Test post', + reply: undefined, + }; + await handlerAgent.post(input); + expect(postMock).toHaveBeenCalledWith(input); + }); + + describe('CreateSkeet', () => { + it('createSkeet should call post with input text and did facets', async () => { + await handlerAgent.createSkeet('Test post @bsky.app'); + const facets = [ + { + $type: 'app.bsky.richtext.facet', + features: [ + { + $type: 'app.bsky.richtext.facet#mention', + did: 'did:plc:z72i7hdynmk6r22z27h6tvur', + }, + ], + index: { + byteEnd: 19, + byteStart: 10, + }, + }, + ]; + expect(postMock).toHaveBeenCalledWith({ + text: 'Test post @bsky.app', + facets: facets, + }); + }); + + it('createSkeet should call post with input text and url facets', async () => { + await handlerAgent.createSkeet('Test post bsky.app'); + const facets = [ + { + features: [ + { + $type: 'app.bsky.richtext.facet#link', + uri: 'https://bsky.app', + }, + ], + index: { + byteEnd: 18, + byteStart: 10, + }, + }, + ]; + expect(postMock).toHaveBeenCalledWith({ + text: 'Test post bsky.app', + facets: facets, + }); + }); + + it('createSkeet should call post with input text and both facets', async () => { + await handlerAgent.createSkeet('Test post bsky.app @bsky.app'); + const facets = [ + { + features: [ + { + $type: 'app.bsky.richtext.facet#link', + uri: 'https://bsky.app', + }, + ], + index: { + byteEnd: 18, + byteStart: 10, + }, + }, + { + $type: 'app.bsky.richtext.facet', + features: [ + { + $type: 'app.bsky.richtext.facet#mention', + did: 'did:plc:z72i7hdynmk6r22z27h6tvur', + }, + ], + index: { + byteEnd: 28, + byteStart: 19, + }, + }, + ]; + expect(postMock).toHaveBeenCalledWith({ + text: 'Test post bsky.app @bsky.app', + facets: facets, + }); }); }); }); diff --git a/tests/agent/HandlerAgentUtils.test.ts b/tests/agent/HandlerAgentUtils.test.ts index 6a17efa..869fe47 100644 --- a/tests/agent/HandlerAgentUtils.test.ts +++ b/tests/agent/HandlerAgentUtils.test.ts @@ -1,20 +1,30 @@ import { - CreateSkeetMessage, - CreateSkeetMessageFactory, - CreateSkeetRecordFactory, HandlerAgent, - JetstreamMessage, - JetstreamMessageFactory, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + JetstreamRecord, + NewSkeetRecord, + NewSkeetRecordFactory, ReplyFactory, } from '../../src'; import { AtpSessionData, BskyAgent } from '@atproto/api'; import dotenv from 'dotenv'; +import fs from 'fs'; dotenv.config(); +process.env.SESSION_DATA_PATH = './tests/temp/utils'; jest.mock('@atproto/api', () => jest.genMockFromModule('@atproto/api')); describe('HandlerAgent', () => { + afterAll(() => { + fs.rmSync('./tests/temp/utils', { + recursive: true, + force: true, + }); + }); + fs.mkdirSync('./tests/temp/utils', { recursive: true }); let handlerAgent: HandlerAgent; const testHandle: string = 'testhandle'; const testPassword: string = 'testpassword'; @@ -37,10 +47,10 @@ describe('HandlerAgent', () => { it('generateURIFromCreateMessage creates expected uri', () => { const did = 'did:plc:12345'; const rkey = 'rkeytest'; - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() + const message: JetstreamEventCommit = JetstreamEventFactory.factory() .fromDid(did) - .rkey(rkey) - .create(); + .commit(JetstreamCommitFactory.factory().rkey(rkey).create()) + .create() as JetstreamEventCommit; const result = handlerAgent.generateURIFromCreateMessage(message); expect(result).toEqual(`at://${did}/app.bsky.feed.post/${rkey}`); @@ -48,17 +58,21 @@ describe('HandlerAgent', () => { describe('postedByAgent', () => { it('should return true when message is same did as bot', () => { - const message: JetstreamMessage = JetstreamMessageFactory.factory() - .fromDid(botDid) - .create(); + const message: JetstreamEventCommit = + JetstreamEventFactory.factory() + .fromDid(botDid) + .commit() + .create() as JetstreamEventCommit; const result = handlerAgent.postedByAgent(message); expect(result).toBe(true); }); it('should return false when message is not same did as bot', () => { - const message: JetstreamMessage = JetstreamMessageFactory.factory() - .fromDid('did:plc:other') - .create(); + const message: JetstreamEventCommit = + JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit() + .create() as JetstreamEventCommit; const result = handlerAgent.postedByAgent(message); expect(result).toBe(false); @@ -68,15 +82,50 @@ describe('HandlerAgent', () => { describe('getPostReply', () => { it('should return reply from message', () => { const reply = ReplyFactory.factory().create(); - const message: CreateSkeetMessage = - CreateSkeetMessageFactory.factory() - .fromDid(botDid) - .record( - CreateSkeetRecordFactory.factory().reply(reply).create() + const message: JetstreamEventCommit = + JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory() + .reply(reply) + .create() + ) + .create() ) - .create(); + .create() as JetstreamEventCommit; const result = handlerAgent.getPostReply(message); expect(result).toBe(reply); }); }); + + // New test to cover the specific lines + describe('special case for commit.record.subject being a string', () => { + it('should return default structure when subject is a string', () => { + const message: JetstreamEventCommit = + JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record({ + subject: 'some string', + } as JetstreamRecord) + .create() + ) + .create() as JetstreamEventCommit; + + const result = handlerAgent.generateReplyFromMessage(message); // Or whichever method is relevant + expect(result).toEqual({ + root: { + uri: '', + cid: '', + }, + parent: { + uri: '', + cid: '', + }, + }); + }); + }); }); diff --git a/tests/firehose/JeststreamSubscription.test.ts b/tests/firehose/JeststreamSubscription.test.ts index 6de53f6..5ff8d6c 100644 --- a/tests/firehose/JeststreamSubscription.test.ts +++ b/tests/firehose/JeststreamSubscription.test.ts @@ -1,13 +1,12 @@ import { - CreateMessage, - CreateMessageFactory, - DeleteMessage, - JetstreamMessageFactory, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, JetstreamSubscription, JetstreamSubscriptionHandlers, MessageHandler, - Record, } from '../../src'; +import WebSocket from 'ws'; describe('JetstreamSubscription', () => { let jetSub: JetstreamSubscription; @@ -35,10 +34,7 @@ describe('JetstreamSubscription', () => { } as unknown as MessageHandler; beforeEach(() => { - jetSub = new JetstreamSubscription( - handlers, - 'ws://localhost:6008/subscribe' - ); + jetSub = new JetstreamSubscription(handlers); (dummyHandler.handle as jest.Mock).mockClear(); }); @@ -62,93 +58,174 @@ describe('JetstreamSubscription', () => { // @ts-ignore handlers.post.c = [dummyHandler]; - const msg: CreateMessage = CreateMessageFactory.make(); + const msg: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .create() + ) + .create() as JetstreamEventCommit; jetSub.handleCreate(msg); expect(dummyHandler.handle).toHaveBeenCalledTimes(1); - expect(dummyHandler.handle).toHaveBeenCalledWith(msg); + expect(dummyHandler.handle).toHaveBeenCalledWith(undefined, msg); }); test('handleCreate like', () => { // @ts-ignore handlers.like.c = [dummyHandler]; - const msg: CreateMessage = CreateMessageFactory.factory() - .collection('app.bsky.feed.like') - .create(); + const msg: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.like') + .create() + ) + .create() as JetstreamEventCommit; jetSub.handleCreate(msg); expect(dummyHandler.handle).toHaveBeenCalledTimes(1); - expect(dummyHandler.handle).toHaveBeenCalledWith(msg); + expect(dummyHandler.handle).toHaveBeenCalledWith(undefined, msg); }); test('handleCreate repost', () => { // @ts-ignore handlers.repost.c = [dummyHandler]; - const msg: CreateMessage = CreateMessageFactory.factory() - .collection('app.bsky.feed.repost') - .create(); + const msg: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.repost') + .create() + ) + .create() as JetstreamEventCommit; jetSub.handleCreate(msg); expect(dummyHandler.handle).toHaveBeenCalledTimes(1); - expect(dummyHandler.handle).toHaveBeenCalledWith(msg); + expect(dummyHandler.handle).toHaveBeenCalledWith(undefined, msg); }); test('handleCreate follow', () => { // @ts-ignore handlers.follow.c = [dummyHandler]; - const msg: CreateMessage = CreateMessageFactory.factory() - .collection('app.bsky.graph.follow') - .create(); + const msg: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.graph.follow') + .create() + ) + .create() as JetstreamEventCommit; jetSub.handleCreate(msg); expect(dummyHandler.handle).toHaveBeenCalledTimes(1); - expect(dummyHandler.handle).toHaveBeenCalledWith(msg); + expect(dummyHandler.handle).toHaveBeenCalledWith(undefined, msg); }); test('handleDelete post', () => { // @ts-ignore handlers.post.d = [dummyHandler]; - const msg: DeleteMessage = JetstreamMessageFactory.factory() - .collection('app.bsky.feed.post') - .isDeletion() - .create(); + const msg: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('delete') + .collection('app.bsky.feed.post') + .create() + ) + .create() as JetstreamEventCommit; jetSub.handleDelete(msg); expect(dummyHandler.handle).toHaveBeenCalledTimes(1); - expect(dummyHandler.handle).toHaveBeenCalledWith(msg); + expect(dummyHandler.handle).toHaveBeenCalledWith(undefined, msg); }); test('handleDelete like', () => { // @ts-ignore handlers.like.d = [dummyHandler]; - const msg: DeleteMessage = JetstreamMessageFactory.factory() - .collection('app.bsky.feed.like') - .isDeletion() - .create(); + const msg: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('delete') + .collection('app.bsky.feed.like') + .create() + ) + .create() as JetstreamEventCommit; jetSub.handleDelete(msg); expect(dummyHandler.handle).toHaveBeenCalledTimes(1); - expect(dummyHandler.handle).toHaveBeenCalledWith(msg); + expect(dummyHandler.handle).toHaveBeenCalledWith(undefined, msg); }); test('handleDelete repost', () => { // @ts-ignore handlers.repost.d = [dummyHandler]; - const msg: DeleteMessage = JetstreamMessageFactory.factory() - .collection('app.bsky.feed.repost') - .isDeletion() - .create(); + const msg: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('delete') + .collection('app.bsky.feed.repost') + .create() + ) + .create() as JetstreamEventCommit; jetSub.handleDelete(msg); expect(dummyHandler.handle).toHaveBeenCalledTimes(1); - expect(dummyHandler.handle).toHaveBeenCalledWith(msg); + expect(dummyHandler.handle).toHaveBeenCalledWith(undefined, msg); }); test('handleDelete follow', () => { // @ts-ignore handlers.follow.d = [dummyHandler]; - const msg: DeleteMessage = JetstreamMessageFactory.factory() - .collection('app.bsky.graph.follow') - .isDeletion() - .create(); + const msg: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('delete') + .collection('app.bsky.graph.follow') + .create() + ) + .create() as JetstreamEventCommit; jetSub.handleDelete(msg); expect(dummyHandler.handle).toHaveBeenCalledTimes(1); - expect(dummyHandler.handle).toHaveBeenCalledWith(msg); + expect(dummyHandler.handle).toHaveBeenCalledWith(undefined, msg); + }); +}); + +describe('JetstreamSubscription', () => { + let jetSub: JetstreamSubscription; + const handlers: JetstreamSubscriptionHandlers = { + post: { + c: [], + d: [], + }, + like: { + c: [], + d: [], + }, + repost: { + c: [], + d: [], + }, + follow: { + c: [], + d: [], + }, + }; + // A dummy message handler for testing + const dummyHandler: MessageHandler = { + handle: jest.fn(), + } as unknown as MessageHandler; + + const closeMock = jest.fn(); + + beforeEach(() => { + jetSub = new JetstreamSubscription( + handlers, + 'ws://localhost:6008/subscribe' + ); + + jetSub.wsClient = { close: closeMock } as unknown as WebSocket; + (dummyHandler.handle as jest.Mock).mockClear(); + }); + + test('close sub closes it', () => { + jetSub.stopSubscription(); + expect(closeMock).toHaveBeenCalled(); }); }); diff --git a/tests/firehose/JetstreamCreateSubscription.test.ts b/tests/firehose/JetstreamCreateSubscription.test.ts new file mode 100644 index 0000000..11d32f3 --- /dev/null +++ b/tests/firehose/JetstreamCreateSubscription.test.ts @@ -0,0 +1,106 @@ +import ws from 'ws'; +import { + DebugLog, + JetstreamSubscription, + JetstreamSubscriptionHandlers, +} from '../../src'; // import this if you have it installed + +describe('JetstreamSubscription createSubscription', () => { + let server: ws.Server | null; + let jetstreamSub: JetstreamSubscription; + const wsURL = 'ws://localhost:1234'; + const handlers: JetstreamSubscriptionHandlers = {}; // fill in handlers required for testing + const warnMock = jest.fn(); + const errorMock = jest.fn(); + const openSubMock = jest.fn(); + const handleCreateMock = jest.fn(); + const handleDeleteMock = jest.fn(); + + beforeEach(() => { + server = new ws.Server({ port: 1234 }); + jetstreamSub = new JetstreamSubscription(handlers, wsURL); + jetstreamSub.handleOpen = openSubMock; + jetstreamSub.handleCreate = handleCreateMock; + jetstreamSub.handleDelete = handleDeleteMock; + + DebugLog.warn = warnMock; + DebugLog.error = errorMock; + }); + afterEach(() => { + server?.close(); + server = null; + jest.clearAllMocks(); + }); + + it( + 'Should run create subscription', + async () => { + const returnedSub = jetstreamSub.createSubscription(); + expect(warnMock).toHaveBeenCalled(); + expect(typeof returnedSub).toBe('object'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + // Expect the handleError not to have been called + expect(errorMock).not.toHaveBeenCalled(); + expect(openSubMock).toHaveBeenCalled(); + + server?.clients.forEach((client) => { + // @ts-ignore + if (client !== ws && client.readyState === ws.OPEN) { + client.send( + Buffer.from( + JSON.stringify({ + kind: 'commit', + commit: { + operation: 'create', + }, + }) + ), + { + binary: true, + } + ); + } + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect(handleCreateMock).toHaveBeenCalled(); + + server?.clients.forEach((client) => { + // @ts-ignore + if (client !== ws && client.readyState === ws.OPEN) { + client.send( + Buffer.from( + JSON.stringify({ + kind: 'commit', + commit: { + operation: 'delete', + }, + }) + ), + { + binary: true, + } + ); + } + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect(handleDeleteMock).toHaveBeenCalled(); + + const createSubscriptionMock = jest.fn(); + jetstreamSub.createSubscription = createSubscriptionMock; + jetstreamSub.restartDelay = 1; + jetstreamSub.stopSubscription(true); + + await new Promise((resolve) => setTimeout(resolve, 500)); + + expect(errorMock).toHaveBeenCalled(); + expect(warnMock).toHaveBeenCalled(); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect(createSubscriptionMock).toHaveBeenCalled(); + }, + 1000 * 10 + ); +}); diff --git a/tests/firehose/SupportingJetstream.test.ts b/tests/firehose/SupportingJetstream.test.ts new file mode 100644 index 0000000..124f142 --- /dev/null +++ b/tests/firehose/SupportingJetstream.test.ts @@ -0,0 +1,62 @@ +import { + JetstreamSubscription, + JetstreamSubscriptionHandlers, + DebugLog, +} from '../../src'; + +// Mock the entire 'ws' module and its WebSocket constructor +jest.mock('ws', () => { + const mockWebSocket = jest.fn().mockImplementation(() => ({ + on: jest.fn(), + close: jest.fn(), + })); + + return { WebSocket: mockWebSocket }; +}); + +describe('JetstreamSubscription', () => { + let mockHandlers: JetstreamSubscriptionHandlers; + let subscription: JetstreamSubscription; + const mockDebugInfo = jest.fn(); + const mockDebugError = jest.fn(); + + beforeEach(() => { + mockHandlers = { + post: { c: [], d: [] }, + like: { c: [], d: [] }, + repost: { c: [], d: [] }, + follow: { c: [], d: [] }, + }; + + subscription = new JetstreamSubscription( + mockHandlers, + 'ws://localhost' + ); + + // Mock the DebugLog methods + DebugLog.info = mockDebugInfo; + DebugLog.error = mockDebugError; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('calls debug when opened', () => { + subscription.handleOpen(); + expect(mockDebugInfo).toHaveBeenCalledWith( + 'FIREHOSE', + 'Connection Opened' + ); + }); + + it('handles errors correctly', () => { + const testError = new Error('Test error'); + subscription.handleError(testError); + expect(mockDebugError).toHaveBeenCalledWith( + 'FIREHOSE', + 'Error: Test error' + ); + expect(subscription.restart).toBe(true); + }); +}); diff --git a/tests/handlers/MessageHandler.test.ts b/tests/handlers/MessageHandler.test.ts index 7fd0414..dffc3b5 100644 --- a/tests/handlers/MessageHandler.test.ts +++ b/tests/handlers/MessageHandler.test.ts @@ -1,11 +1,11 @@ import { AbstractMessageAction, AbstractValidator, - CreateSkeetMessage, DebugLog, HandlerAgent, - JetstreamMessage, - JetstreamMessageFactory, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, MessageHandler, } from '../../src'; @@ -24,15 +24,15 @@ describe('MessageHandler', () => { mockValidatorShouldTrigger = jest .fn() .mockImplementation( - (message: CreateSkeetMessage, agent: HandlerAgent) => { - return message.opType === 'c'; + (agent: HandlerAgent, message: JetstreamEventCommit) => { + return message.commit.operation === 'create'; } ); mockActionHandle = jest .fn() .mockImplementation( - (message: CreateSkeetMessage, agent: HandlerAgent) => { - if (message.seq === 3) { + (agent: HandlerAgent, message: JetstreamEventCommit) => { + if (message.did === 'err') { throw new Error('error'); } return; @@ -63,31 +63,43 @@ describe('MessageHandler', () => { describe('handle', () => { it('should run actions when opType is c', async () => { //make CreateSkeetMessage - const message: JetstreamMessage = JetstreamMessageFactory.make(); - await messageHandler.handle(message); + const message: JetstreamEventCommit = + JetstreamEventFactory.factory() + .commit() + .create() as JetstreamEventCommit; + await messageHandler.handle(undefined, message); expect(mockValidatorShouldTrigger).toHaveBeenCalled(); expect(mockActionHandle).toHaveBeenCalledWith( - message, - mockedHandlerAgent + mockedHandlerAgent, + message ); }); it('should run not actions when opType is d', async () => { - const message: JetstreamMessage = JetstreamMessageFactory.factory() - .isDeletion() - .create(); - await messageHandler.handle(message); + const message: JetstreamEventCommit = + JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('delete') + .create() + ) + .create() as JetstreamEventCommit; + + await messageHandler.handle(undefined, message); expect(mockValidatorShouldTrigger).toHaveBeenCalled(); expect(mockActionHandle).not.toHaveBeenCalled(); }); it('should debug log error when handle throws error', async () => { - const message: JetstreamMessage = JetstreamMessageFactory.factory() - .seq(3) - .create(); - await messageHandler.handle(message); + const message: JetstreamEventCommit = + JetstreamEventFactory.factory() + .fromDid('err') + .commit() + .create() as JetstreamEventCommit; + + await messageHandler.handle(undefined, message); expect(mockValidatorShouldTrigger).toHaveBeenCalled(); expect(mockActionHandle).toHaveBeenCalled(); diff --git a/tests/handlers/TestHandler.test.ts b/tests/handlers/TestHandler.test.ts index 65cef56..827cf8d 100644 --- a/tests/handlers/TestHandler.test.ts +++ b/tests/handlers/TestHandler.test.ts @@ -1,18 +1,102 @@ import { + AbstractAction, AbstractMessageAction, AbstractValidator, CreateSkeetMessage, DebugLog, HandlerAgent, - JetstreamMessage, - JetstreamMessageFactory, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, TestHandler, + TestMessageHandler, } from '../../src'; describe('TestHandler', () => { let testHandler: TestHandler; let mockedHandlerAgent: HandlerAgent; let mockedValidators: AbstractValidator[]; + let mockedActions: AbstractAction[]; + let mockValidatorShouldTrigger: jest.Mock; + let mockActionHandle: jest.Mock; + let mockDebugError: jest.Mock; + + beforeEach(() => { + mockDebugError = jest.fn(); + DebugLog.error = mockDebugError; + mockValidatorShouldTrigger = jest + .fn() + .mockImplementation((agent: HandlerAgent, ...args: any) => { + return args[0] === 1; + }); + mockActionHandle = jest + .fn() + .mockImplementation((agent: HandlerAgent, ...args: any) => { + if (args[1] === 1) { + throw new Error('error'); + } + return; + }); + mockedHandlerAgent = {} as HandlerAgent; + mockedValidators = [ + { + shouldTrigger: mockValidatorShouldTrigger, + } as unknown as AbstractValidator, + ]; + mockedActions = [ + { + handle: mockActionHandle, + } as unknown as AbstractAction, + ]; + testHandler = new TestHandler( + mockedValidators, + mockedActions, + mockedHandlerAgent + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Should throw error when calling make', () => { + expect(TestHandler.make).toThrow( + 'Method Not Implemented! Use constructor.' + ); + }); + + describe('handle', () => { + it('should run actions when arg is 1', async () => { + await testHandler.handle(undefined, 1); + + expect(mockValidatorShouldTrigger).toHaveBeenCalled(); + expect(mockActionHandle).toHaveBeenCalledWith( + mockedHandlerAgent, + 1 + ); + }); + + it('should run not actions when arg is 0', async () => { + await testHandler.handle(undefined, 0); + + expect(mockValidatorShouldTrigger).toHaveBeenCalled(); + expect(mockActionHandle).not.toHaveBeenCalled(); + }); + + it('should debug log error when handle throws error', async () => { + await testHandler.handle(undefined, 1, 1); + + expect(mockValidatorShouldTrigger).toHaveBeenCalled(); + expect(mockActionHandle).toHaveBeenCalled(); + expect(mockDebugError).toHaveBeenCalled(); + }); + }); +}); + +describe('TestMessageHandler', () => { + let testMessageHandler: TestMessageHandler; + let mockedHandlerAgent: HandlerAgent; + let mockedValidators: AbstractValidator[]; let mockedActions: AbstractMessageAction[]; let mockValidatorShouldTrigger: jest.Mock; let mockActionHandle: jest.Mock; @@ -24,15 +108,15 @@ describe('TestHandler', () => { mockValidatorShouldTrigger = jest .fn() .mockImplementation( - (message: CreateSkeetMessage, agent: HandlerAgent) => { - return message.opType === 'c'; + (agent: HandlerAgent, message: JetstreamEventCommit) => { + return message.commit.operation === 'create'; } ); mockActionHandle = jest .fn() .mockImplementation( - (message: CreateSkeetMessage, agent: HandlerAgent) => { - if (message.seq === 3) { + (agent: HandlerAgent, message: CreateSkeetMessage) => { + if (message.did === 'err') { throw new Error('error'); } return; @@ -49,7 +133,7 @@ describe('TestHandler', () => { handle: mockActionHandle, } as unknown as AbstractMessageAction, ]; - testHandler = new TestHandler( + testMessageHandler = new TestMessageHandler( mockedValidators, mockedActions, mockedHandlerAgent @@ -60,45 +144,55 @@ describe('TestHandler', () => { jest.clearAllMocks(); }); - it('Should throw error when calling make', () => { - expect(TestHandler.make).toThrow( - 'Method Not Implemented! Use constructor.' - ); - }); + // it('Should throw error when calling make', () => { + // expect(TestMessageHandler.make).toThrow( + // 'Method Not Implemented! Use constructor.' + // ); + // }); describe('handle', () => { it('should run actions when opType is c', async () => { //make CreateSkeetMessage - const message: JetstreamMessage = JetstreamMessageFactory.make(); - await testHandler.handle(message); + const message: JetstreamEventCommit = + JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .create() + ) + .create() as JetstreamEventCommit; + await testMessageHandler.handle(undefined, message); expect(mockValidatorShouldTrigger).toHaveBeenCalled(); expect(mockActionHandle).toHaveBeenCalledWith( - message, - mockedHandlerAgent + mockedHandlerAgent, + message ); }); it('should run not actions when opType is d', async () => { - const message: JetstreamMessage = JetstreamMessageFactory.factory() - .isDeletion() - .create(); - await testHandler.handle(message); + const message: JetstreamEventCommit = + JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('delete') + .create() + ) + .create() as JetstreamEventCommit; + + await testMessageHandler.handle(undefined, message); expect(mockValidatorShouldTrigger).toHaveBeenCalled(); expect(mockActionHandle).not.toHaveBeenCalled(); }); it('should debug log error when handle throws error', async () => { - const message: JetstreamMessage = { - collection: '', - did: '', - opType: 'c', - rkey: '', - seq: 3, - cid: 'cid', - }; - await testHandler.handle(message); + const message: JetstreamEventCommit = + JetstreamEventFactory.factory() + .fromDid('err') + .commit() + .create() as JetstreamEventCommit; + await testMessageHandler.handle(undefined, message); expect(mockValidatorShouldTrigger).toHaveBeenCalled(); expect(mockActionHandle).toHaveBeenCalled(); diff --git a/tests/handlers/premade-handlers/GoodAndBadBotHandler.test.ts b/tests/handlers/premade-handlers/GoodAndBadBotHandler.test.ts index 22648e0..c4bdb28 100644 --- a/tests/handlers/premade-handlers/GoodAndBadBotHandler.test.ts +++ b/tests/handlers/premade-handlers/GoodAndBadBotHandler.test.ts @@ -1,25 +1,42 @@ import { BadBotHandler, - CreateSkeetMessage, - CreateSkeetMessageFactory, - CreateSkeetRecordFactory, GoodBotHandler, HandlerAgent, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, ReplyFactory, } from '../../../src'; import { BskyAgent } from '@atproto/api'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/GoodBad'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; describe('Good and Bad Bot Handler', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + let goodBotHandler: GoodBotHandler; let badBotHandler: BadBotHandler; // let handlerAgent: HandlerAgent; - let message: CreateSkeetMessage; + let message: JetstreamEventCommit; const mockCreateSkeet = jest.fn(); const mockHasPostReply = jest .fn() - .mockImplementation((message: CreateSkeetMessage) => { + .mockImplementation((message: JetstreamEventCommit) => { return ( - 'reply' in message.record && message.record?.reply !== undefined + // @ts-ignore + 'reply' in message.commit.record && + message.commit.record?.reply !== undefined ); }); const mockGetDidFromUri = jest.fn().mockImplementation((uri: string) => { @@ -52,18 +69,27 @@ describe('Good and Bad Bot Handler', () => { describe('Good Bot Handler', () => { it('GoodBotHandler Does run actions with default when post is reply to bot and good bot', async () => { goodBotHandler = GoodBotHandler.make(handlerAgent); - message = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory() - .reply(ReplyFactory.factory().replyTo(botDid).create()) - .text('good bot') + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory() + .reply( + ReplyFactory.factory() + .replyTo(botDid) + .create() + ) + .text('good bot') + .create() + ) .create() ) - .create(); - await goodBotHandler.handle(message); + .create() as JetstreamEventCommit; + await goodBotHandler.handle(undefined, message); expect(mockHasPostReply).toHaveBeenCalledWith(message); expect(mockGetDidFromUri).toHaveBeenCalledWith( - message?.record?.reply?.parent.uri + message?.commit?.record?.reply?.parent.uri ); expect(mockCreateSkeet).toHaveBeenCalledWith( 'Thank you đŸĨš', @@ -73,18 +99,28 @@ describe('Good and Bad Bot Handler', () => { it('GoodBotHandler Does run actions with input when post is reply to bot and good bot', async () => { goodBotHandler = GoodBotHandler.make(handlerAgent, 'test'); - message = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory() - .reply(ReplyFactory.factory().replyTo(botDid).create()) - .text('good bot') + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory() + .reply( + ReplyFactory.factory() + .replyTo(botDid) + .create() + ) + .text('good bot') + .create() + ) .create() ) - .create(); - await goodBotHandler.handle(message); + .create() as JetstreamEventCommit; + + await goodBotHandler.handle(undefined, message); expect(mockHasPostReply).toHaveBeenCalledWith(message); expect(mockGetDidFromUri).toHaveBeenCalledWith( - message?.record?.reply?.parent.uri + message?.commit?.record?.reply?.parent.uri ); expect(mockCreateSkeet).toHaveBeenCalledWith( 'test', @@ -94,52 +130,74 @@ describe('Good and Bad Bot Handler', () => { it('GoodBotHandler Does not run actions when post is reply to bot, but not good bot', async () => { goodBotHandler = GoodBotHandler.make(handlerAgent); - message = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory() - .reply(ReplyFactory.factory().replyTo(botDid).create()) - .text('test') + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory() + .reply( + ReplyFactory.factory() + .replyTo(botDid) + .create() + ) + .text('test') + .create() + ) .create() ) - .create(); - await goodBotHandler.handle(message); + .create() as JetstreamEventCommit; + await goodBotHandler.handle(undefined, message); expect(mockHasPostReply).toHaveBeenCalledWith(message); expect(mockGetDidFromUri).toHaveBeenCalledWith( - message?.record?.reply?.parent.uri + message?.commit?.record?.reply?.parent.uri ); expect(mockCreateSkeet).not.toHaveBeenCalled(); }); it('GoodBotHandler Does not run actions when post is not reply to bot', async () => { goodBotHandler = GoodBotHandler.make(handlerAgent); - message = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory() - .reply( - ReplyFactory.factory() - .replyTo('did:plc:other') + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory() + .reply( + ReplyFactory.factory() + .replyTo('did:plc:other') + .create() + ) + .text('good bot') .create() ) - .text('good bot') .create() ) - .create(); - await goodBotHandler.handle(message); + .create() as JetstreamEventCommit; + + await goodBotHandler.handle(undefined, message); expect(mockHasPostReply).toHaveBeenCalledWith(message); expect(mockGetDidFromUri).toHaveBeenCalledWith( - message?.record?.reply?.parent.uri + message?.commit?.record?.reply?.parent.uri ); expect(mockCreateSkeet).not.toHaveBeenCalled(); }); it('GoodBotHandler Does not run actions when post is not reply', async () => { goodBotHandler = GoodBotHandler.make(handlerAgent); - message = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory().text('good bot').create() + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory() + .text('good bot') + .create() + ) + .create() ) - .create(); - await goodBotHandler.handle(message); + .create() as JetstreamEventCommit; + await goodBotHandler.handle(undefined, message); expect(mockHasPostReply).toHaveBeenCalledWith(message); expect(mockGetDidFromUri).not.toHaveBeenCalled(); expect(mockCreateSkeet).not.toHaveBeenCalled(); @@ -149,18 +207,27 @@ describe('Good and Bad Bot Handler', () => { describe('Bad Bot Handler', () => { it('BadBotHandler Does run actions with default when post is reply to bot and bad bot', async () => { badBotHandler = BadBotHandler.make(handlerAgent); - message = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory() - .reply(ReplyFactory.factory().replyTo(botDid).create()) - .text('bad bot') + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory() + .reply( + ReplyFactory.factory() + .replyTo(botDid) + .create() + ) + .text('bad bot') + .create() + ) .create() ) - .create(); - await badBotHandler.handle(message); + .create() as JetstreamEventCommit; + await badBotHandler.handle(undefined, message); expect(mockHasPostReply).toHaveBeenCalledWith(message); expect(mockGetDidFromUri).toHaveBeenCalledWith( - message?.record?.reply?.parent.uri + message?.commit?.record?.reply?.parent.uri ); expect(mockCreateSkeet).toHaveBeenCalledWith( "I'm sorry 😓", @@ -170,18 +237,28 @@ describe('Good and Bad Bot Handler', () => { it('BadBotHandler Does run actions with input when post is reply to bot and bad bot', async () => { badBotHandler = BadBotHandler.make(handlerAgent, 'test'); - message = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory() - .reply(ReplyFactory.factory().replyTo(botDid).create()) - .text('bad bot') + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory() + .reply( + ReplyFactory.factory() + .replyTo(botDid) + .create() + ) + .text('bad bot') + .create() + ) .create() ) - .create(); - await badBotHandler.handle(message); + .create() as JetstreamEventCommit; + + await badBotHandler.handle(undefined, message); expect(mockHasPostReply).toHaveBeenCalledWith(message); expect(mockGetDidFromUri).toHaveBeenCalledWith( - message?.record?.reply?.parent.uri + message?.commit?.record?.reply?.parent.uri ); expect(mockCreateSkeet).toHaveBeenCalledWith( 'test', @@ -191,52 +268,75 @@ describe('Good and Bad Bot Handler', () => { it('BadBotHandler Does not run actions when post is reply to bot, but not bad bot', async () => { badBotHandler = BadBotHandler.make(handlerAgent); - message = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory() - .reply(ReplyFactory.factory().replyTo(botDid).create()) - .text('good bot') + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory() + .reply( + ReplyFactory.factory() + .replyTo(botDid) + .create() + ) + .text('good bot') + .create() + ) .create() ) - .create(); - await badBotHandler.handle(message); + .create() as JetstreamEventCommit; + + await badBotHandler.handle(undefined, message); expect(mockHasPostReply).toHaveBeenCalledWith(message); expect(mockGetDidFromUri).toHaveBeenCalledWith( - message?.record?.reply?.parent.uri + message?.commit?.record?.reply?.parent.uri ); expect(mockCreateSkeet).not.toHaveBeenCalled(); }); it('BadBotHandler Does not run actions when post is not reply to bot', async () => { badBotHandler = BadBotHandler.make(handlerAgent); - message = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory() - .reply( - ReplyFactory.factory() - .replyTo('did:plc:other') + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory() + .reply( + ReplyFactory.factory() + .replyTo('did:plc:other') + .create() + ) + .text('bad bot') .create() ) - .text('bad bot') .create() ) - .create(); - await badBotHandler.handle(message); + .create() as JetstreamEventCommit; + + await badBotHandler.handle(undefined, message); expect(mockHasPostReply).toHaveBeenCalledWith(message); expect(mockGetDidFromUri).toHaveBeenCalledWith( - message?.record?.reply?.parent.uri + message?.commit?.record?.reply?.parent.uri ); expect(mockCreateSkeet).not.toHaveBeenCalled(); }); it('BadBotHandler Does not run actions when post is not reply', async () => { badBotHandler = BadBotHandler.make(handlerAgent); - message = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory().text('bad bot').create() + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory() + .text('bad bot') + .create() + ) + .create() ) - .create(); - await badBotHandler.handle(message); + .create() as JetstreamEventCommit; + await badBotHandler.handle(undefined, message); expect(mockHasPostReply).toHaveBeenCalledWith(message); expect(mockGetDidFromUri).not.toHaveBeenCalled(); expect(mockCreateSkeet).not.toHaveBeenCalled(); diff --git a/tests/handlers/premade-handlers/OfflineHandler.test.ts b/tests/handlers/premade-handlers/OfflineHandler.test.ts index 3cc3cc8..bb0ce9f 100644 --- a/tests/handlers/premade-handlers/OfflineHandler.test.ts +++ b/tests/handlers/premade-handlers/OfflineHandler.test.ts @@ -1,24 +1,37 @@ import { - BadBotHandler, - CreateSkeetMessage, - CreateSkeetMessageFactory, - CreateSkeetRecordFactory, - GoodBotHandler, HandlerAgent, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, OfflineHandler, - ReplyFactory, } from '../../../src'; import { BskyAgent } from '@atproto/api'; +import fs from 'fs'; +import dotenv from 'dotenv'; -describe('Good Bot Handler', () => { +const sessPath = './tests/temp/handler/offline'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('Offline Handler', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); let offlineHandler: OfflineHandler; - let message: CreateSkeetMessage; + let message: JetstreamEventCommit; const mockCreateSkeet = jest.fn(); const mockHasPostReply = jest .fn() - .mockImplementation((message: CreateSkeetMessage) => { + .mockImplementation((message: JetstreamEventCommit) => { return ( - 'reply' in message.record && message.record?.reply !== undefined + // @ts-ignore + 'reply' in message.commit.record && + message.commit.record?.reply !== undefined ); }); const mockGetDidFromUri = jest.fn().mockImplementation((uri: string) => { @@ -49,10 +62,18 @@ describe('Good Bot Handler', () => { it('OfflineHandler Does run actions with defaults when post is command', async () => { offlineHandler = OfflineHandler.make(handlerAgent, 'test'); - message = CreateSkeetMessageFactory.factory() - .record(CreateSkeetRecordFactory.factory().text('!test').create()) - .create(); - await offlineHandler.handle(message); + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory().text('!test').create() + ) + .create() + ) + .create() as JetstreamEventCommit; + + await offlineHandler.handle(undefined, message); expect(mockCreateSkeet).toHaveBeenCalledWith( 'Bot functionality offline', handlerAgent.generateReplyFromMessage(message) @@ -61,10 +82,17 @@ describe('Good Bot Handler', () => { it('OfflineHandler Does run actions with input when post is command', async () => { offlineHandler = OfflineHandler.make(handlerAgent, 'test', 'output'); - message = CreateSkeetMessageFactory.factory() - .record(CreateSkeetRecordFactory.factory().text('test!').create()) - .create(); - await offlineHandler.handle(message); + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory().text('test!').create() + ) + .create() + ) + .create() as JetstreamEventCommit; + await offlineHandler.handle(undefined, message); expect(mockCreateSkeet).toHaveBeenCalledWith( 'output', handlerAgent.generateReplyFromMessage(message) @@ -73,10 +101,17 @@ describe('Good Bot Handler', () => { it('OfflineHandler Does not run actions when post is not command', async () => { offlineHandler = OfflineHandler.make(handlerAgent, 'test'); - message = CreateSkeetMessageFactory.factory() - .record(CreateSkeetRecordFactory.factory().text('blah').create()) - .create(); - await offlineHandler.handle(message); + message = JetstreamEventFactory.factory() + .fromDid('did:plc:other') + .commit( + JetstreamCommitFactory.factory() + .record( + NewSkeetRecordFactory.factory().text('blah').create() + ) + .create() + ) + .create() as JetstreamEventCommit; + await offlineHandler.handle(undefined, message); expect(mockCreateSkeet).not.toHaveBeenCalled(); }); }); diff --git a/tests/handlers/skeet/CreateSkeetHandler.test.ts b/tests/handlers/skeet/CreateSkeetHandler.test.ts deleted file mode 100644 index 9a22179..0000000 --- a/tests/handlers/skeet/CreateSkeetHandler.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - AbstractMessageAction, - AbstractValidator, - CreateSkeetHandler, - CreateSkeetMessage, - CreateSkeetMessageFactory, - CreateSkeetRecord, - DebugLog, - HandlerAgent, -} from '../../../src'; - -describe('CreateSkeetHandler', () => { - let createSkeetHandler: CreateSkeetHandler; - let mockedHandlerAgent: HandlerAgent; - let mockedValidators: AbstractValidator[]; - let mockedActions: AbstractMessageAction[]; - let mockValidatorShouldTrigger: jest.Mock; - let mockActionHandle: jest.Mock; - let mockDebugError: jest.Mock; - - beforeEach(() => { - mockDebugError = jest.fn(); - DebugLog.error = mockDebugError; - mockValidatorShouldTrigger = jest - .fn() - .mockImplementation( - (message: CreateSkeetMessage, agent: HandlerAgent) => { - return message.opType === 'c'; - } - ); - mockActionHandle = jest - .fn() - .mockImplementation( - (message: CreateSkeetMessage, agent: HandlerAgent) => { - if (message.seq === 3) { - throw new Error('error'); - } - return; - } - ); - mockedHandlerAgent = {} as HandlerAgent; - mockedValidators = [ - { - shouldTrigger: mockValidatorShouldTrigger, - } as unknown as AbstractValidator, - ]; - mockedActions = [ - { - handle: mockActionHandle, - } as unknown as AbstractMessageAction, - ]; - createSkeetHandler = CreateSkeetHandler.make( - mockedValidators, - mockedActions, - mockedHandlerAgent - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('handle', () => { - it('should run actions when opType is c', async () => { - //make CreateSkeetMessage - const message: CreateSkeetMessage = - CreateSkeetMessageFactory.make(); - await createSkeetHandler.handle(message); - - expect(mockValidatorShouldTrigger).toHaveBeenCalled(); - expect(mockActionHandle).toHaveBeenCalledWith( - message, - mockedHandlerAgent - ); - }); - - it('should run not actions when opType is d', async () => { - const message: CreateSkeetMessage = - CreateSkeetMessageFactory.factory().isDeletion().create(); - await createSkeetHandler.handle(message); - - expect(mockValidatorShouldTrigger).toHaveBeenCalled(); - expect(mockActionHandle).not.toHaveBeenCalled(); - }); - - it('should not run validators when handle throws error', async () => { - const message: CreateSkeetMessage = - CreateSkeetMessageFactory.factory().seq(3).create(); - await createSkeetHandler.handle(message); - - expect(mockValidatorShouldTrigger).toHaveBeenCalled(); - expect(mockActionHandle).toHaveBeenCalled(); - expect(mockDebugError).toHaveBeenCalled(); - }); - }); -}); diff --git a/tests/subscriptions/IntervalSubscription.test.ts b/tests/subscriptions/IntervalSubscription.test.ts new file mode 100644 index 0000000..4a7c4ca --- /dev/null +++ b/tests/subscriptions/IntervalSubscription.test.ts @@ -0,0 +1,115 @@ +// Mocking AbstractHandler +import { + AbstractHandler, + AbstractMessageAction, + AbstractValidator, + HandlerAgent, + IntervalSubscription, + IntervalSubscriptionHandlers, +} from '../../src'; + +// Mock Class +class MockHandler extends AbstractHandler { + handle = jest.fn(); +} + +describe('IntervalSubscriptions', function () { + const mockedHandlerAgent: HandlerAgent = {} as HandlerAgent; + const mockValidatorShouldTrigger = jest.fn().mockReturnValue(true); + const mockActionHandle = jest.fn(); + const mockedValidators: AbstractValidator[] = [ + { + shouldTrigger: mockValidatorShouldTrigger, + } as unknown as AbstractValidator, + ]; + const mockedActions: AbstractMessageAction[] = [ + { + handle: mockActionHandle, + } as unknown as AbstractMessageAction, + ]; + let intervalSubs: IntervalSubscription; + + const handler = new MockHandler( + mockedValidators, + mockedActions, + mockedHandlerAgent + ); + const intervalHandlers: IntervalSubscriptionHandlers = [ + { + intervalSeconds: 1, + handlers: [handler], + }, + ]; + + let mockSetInterval: jest.SpyInstance; + let mockClearInterval: jest.SpyInstance; + + beforeEach(() => { + intervalSubs = new IntervalSubscription(intervalHandlers); + mockSetInterval = jest.spyOn(global, 'setInterval'); + mockClearInterval = jest.spyOn(global, 'clearInterval'); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); // clear all function calls between tests + }); + + // ... + + describe('createSubscription', function () { + // ... + + it('should call the handler handle function for each handler after each interval', () => { + intervalSubs.createSubscription(); + + // Advance timers to imitate the passage of time + jest.advanceTimersByTime( + intervalHandlers[0].intervalSeconds * 1000 + ); + + // Assert that handle has been called with `undefined` + expect(handler.handle).toHaveBeenCalledWith(undefined); + }); + }); + + describe('stopSubscription', function () { + it('should stop the intervals for subscription', () => { + intervalSubs.createSubscription(); + jest.advanceTimersByTime( + intervalHandlers[0].intervalSeconds * 1000 + ); + intervalSubs.stopSubscription(); + + // Check that intervals have been cleared + expect(intervalSubs.intervals.length).toBe(0); + + // Assert that handle has been called only once before stopSubscription + expect(handler.handle).toHaveBeenCalledTimes(1); + }); + }); + + describe('restartSubscription', function () { + it('should stop and then start the subscription', () => { + // Prepare spies for methods + const stopSpy = jest.spyOn(intervalSubs, 'stopSubscription'); + const startSpy = jest.spyOn(intervalSubs, 'createSubscription'); + + intervalSubs.createSubscription(); + jest.runOnlyPendingTimers(); + expect(handler.handle).toHaveBeenCalledTimes(1); + + // Call the method under test + intervalSubs.restartSubscription(); + + // Check that both methods were called + expect(stopSpy).toHaveBeenCalledTimes(1); + expect(startSpy).toHaveBeenCalledTimes(2); // At the beginning and after restartSubscription + + jest.runOnlyPendingTimers(); + // Assert that handle has been called twice (once before restart and once after) + expect(handler.handle).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/tests/types/factories/MessageFactories.test.ts b/tests/types/factories/MessageFactories.test.ts index cbcc8f6..640a645 100644 --- a/tests/types/factories/MessageFactories.test.ts +++ b/tests/types/factories/MessageFactories.test.ts @@ -1,151 +1,226 @@ import { - CollectionType, - CreateMessage, - CreateMessageFactory, - CreateSkeetMessage, - CreateSkeetMessageFactory, - CreateSkeetRecordFactory, - JetstreamMessage, - JetstreamMessageFactory, - OperationType, - Record, - RecordFactory, + JetstreamEvent, + JetstreamEventFactory, + JetstreamCommit, + JetstreamCommitFactory, + JetstreamIdentity, + JetstreamIdentityFactory, + JetstreamAccount, + JetstreamAccountFactory, + NewSkeetRecordFactory, + NewSkeetRecord, } from '../../../src'; -describe('JetstreamMessageFactory', () => { - let factory: JetstreamMessageFactory; - let defaultJetstreamMessage: JetstreamMessage; +describe('JetstreamEventFactory', () => { + let factory: JetstreamEventFactory; + let defaultEvent: JetstreamEvent; + beforeEach(() => { - factory = JetstreamMessageFactory.factory(); - defaultJetstreamMessage = { - cid: '', - collection: 'app.bsky.feed.post', - did: '', - opType: 'c', - rkey: '', - seq: 0, - }; + factory = JetstreamEventFactory.factory(); + defaultEvent = JetstreamEventFactory.factory().commit().create(); + }); + + it('creates a new JetstreamEvent with factory, and a default event with create', () => { + const event = factory.commit().create(); + expect(event).toEqual(defaultEvent); }); - it('creates a new JetstreamMessageFactory with Factory, and a default JetstreamMessage with create', () => { - const message = factory.create(); - expect(message).toEqual(defaultJetstreamMessage); - }); - //collection - it('Updates the JetstreamMessage.collection with a given collection', () => { - const collection: CollectionType = 'app.bsky.feed.like'; - factory.collection(collection); - defaultJetstreamMessage.collection = collection; - expect(factory.create()).toEqual(defaultJetstreamMessage); - }); - // optType - it('Updates the JetstreamMessage.opType with a given opType', () => { - const opType: OperationType = 'c'; - factory.opType(opType); - defaultJetstreamMessage.opType = opType; - expect(factory.create()).toEqual(defaultJetstreamMessage); - }); - // isCreation - it("Updates the JetstreamMessage.opType with a 'c' using isCreation", () => { - factory.isCreation(); - defaultJetstreamMessage.opType = 'c'; - expect(factory.create()).toEqual(defaultJetstreamMessage); - }); - // isDeletion - it("Updates the JetstreamMessage.opType with a 'd' using isDeletion", () => { - factory.isDeletion(); - defaultJetstreamMessage.opType = 'd'; - expect(factory.create()).toEqual(defaultJetstreamMessage); - }); - // fromDid - it('Updates the JetstreamMessage.did with a given did', () => { - const did = 'did:plc:test'; - factory.fromDid(did); - defaultJetstreamMessage.did = did; - expect(factory.create()).toEqual(defaultJetstreamMessage); - }); - //rkey - it('Updates the JetstreamMessage.rkey with a given rkey', () => { - const rkey = 'testrkey'; - factory.rkey(rkey); - defaultJetstreamMessage.rkey = rkey; - expect(factory.create()).toEqual(defaultJetstreamMessage); - }); - // cid - it('Updates the JetstreamMessage.cid with a given cid', () => { - const cid = 'testcid'; - factory.cid(cid); - defaultJetstreamMessage.cid = cid; - expect(factory.create()).toEqual(defaultJetstreamMessage); - }); - // seq - it('Updates the JetstreamMessage.seq with a given seq', () => { - const seq = 100; - factory.seq(seq); - defaultJetstreamMessage.seq = seq; - expect(factory.create()).toEqual(defaultJetstreamMessage); + it('updates the JetstreamEvent DID', () => { + const did = 'did:plc:custom'; + const event = factory.fromDid(did).commit().create(); + defaultEvent.did = did; + expect(event).toEqual(defaultEvent); + }); + + it('attaches a commit to the JetstreamEvent', () => { + const commit = JetstreamCommitFactory.make(); + const event = factory.commit(commit).create(); + defaultEvent.commit = commit; + expect(event).toEqual(defaultEvent); }); }); -describe('CreateMessageFactory', () => { - const factory: CreateMessageFactory = CreateMessageFactory.factory(); - let defaultCreateMessage: CreateMessage; +describe('JetstreamCommitFactory', () => { + let factory: JetstreamCommitFactory; + let defaultCommit: JetstreamCommit; beforeEach(() => { - defaultCreateMessage = { - cid: '', + factory = JetstreamCommitFactory.factory(); + defaultCommit = { collection: 'app.bsky.feed.post', - did: '', - opType: 'c', - rkey: '', - seq: 0, - record: { - $type: 'app.bsky.feed.post', - createdAt: '', - }, + operation: 'create', + rkey: 'examplerkey', + cid: 'examplecid', + rev: 'examplerev', + record: undefined, }; }); - it('creates a new CreateMessageFactory with factory, and a default CreateMessage with create', () => { - const message = factory.create(); - expect(message).toEqual(defaultCreateMessage); + it('creates a new JetstreamCommit with factory, and a default commit with create', () => { + const commit = factory.create(); + expect(commit).toEqual(defaultCommit); + }); + + it('updates the JetstreamCommit operation', () => { + const operation = 'update'; + const commit = factory.operation(operation).create(); + defaultCommit.operation = operation; + expect(commit).toEqual(defaultCommit); }); - //record - it('Updates the createMessage with a given record', () => { - const record: Record = RecordFactory.make(); - factory.record(record); - defaultCreateMessage.record = record; - expect(factory.create()).toEqual(defaultCreateMessage); + + it('updates the JetstreamCommit collection', () => { + const collection = 'app.bsky.feed.like'; + const commit = factory.collection(collection).create(); + defaultCommit.collection = collection; + expect(commit).toEqual(defaultCommit); + }); + + it('updates the JetstreamCommit rkey', () => { + const rkey = 'newrkey'; + const commit = factory.rkey(rkey).create(); + defaultCommit.rkey = rkey; + expect(commit).toEqual(defaultCommit); + }); + + it('attaches a record to the JetstreamCommit', () => { + const record = NewSkeetRecordFactory.factory() + .text('sample message') + .create(); + const commit = factory.record(record).create(); + defaultCommit.record = record; + expect(commit).toEqual(defaultCommit); }); }); -describe('CreateSkeetMessageFactory', () => { - let factory: CreateSkeetMessageFactory; - let defaultCreateSkeetMessage: CreateSkeetMessage; +describe('JetstreamIdentityFactory', () => { + let factory: JetstreamIdentityFactory; + let defaultIdentity: JetstreamIdentity; beforeEach(() => { - factory = CreateSkeetMessageFactory.factory(); - defaultCreateSkeetMessage = { - cid: '', - collection: 'app.bsky.feed.post', - did: '', - opType: 'c', - rkey: '', + factory = JetstreamIdentityFactory.factory(); + defaultIdentity = { + did: 'did:plc:example', + handle: 'handle.example', seq: 0, - record: CreateSkeetRecordFactory.factory().create(), + time: '', }; }); - it('creates a new CreateSkeetMessageFactory with factory, and a default CreateMessage with create', () => { - const message = factory.create(); - defaultCreateSkeetMessage.record.createdAt = message.record.createdAt; - expect(message).toEqual(defaultCreateSkeetMessage); + it('creates a new JetstreamIdentity with factory, and a default identity with create', () => { + const identity = factory.create(); + expect(identity).toEqual(defaultIdentity); + }); + + it('updates the JetstreamIdentity handle', () => { + const handle = 'custom.handle'; + const identity = factory.handle(handle).create(); + defaultIdentity.handle = handle; + expect(identity).toEqual(defaultIdentity); + }); +}); + +describe('JetstreamAccountFactory', () => { + let factory: JetstreamAccountFactory; + let defaultAccount: JetstreamAccount; + + beforeEach(() => { + factory = JetstreamAccountFactory.factory(); + defaultAccount = JetstreamAccountFactory.make(); + }); + + it('creates a new JetstreamAccount with factory, and a default account with create', () => { + const account = factory.create(); + defaultAccount.time = account.time; + expect(account).toEqual(defaultAccount); + }); + + it('deactivates a JetstreamAccount', () => { + const account = factory.deactivate().create(); + defaultAccount.active = false; + expect(account).toEqual(defaultAccount); + }); + + it('updates the JetstreamAccount sequence', () => { + const seq = 42; + const account = factory.sequence(seq).create(); + defaultAccount.seq = seq; + expect(account).toEqual(defaultAccount); + }); + + it('updates the JetstreamAccount sequence', () => { + const account = JetstreamAccountFactory.make(); + + account.time = defaultAccount.time; + expect(account).toEqual(defaultAccount); + }); +}); + +describe('Additional Tests for Jetstream Factories', () => { + describe('JetstreamCommitFactory', () => { + it('should update text in the commit record correctly', () => { + const factory = JetstreamCommitFactory.factory(); + const text = 'new text'; + const commit = factory.text(text).create(); + if (commit.record !== undefined) { + if ('text' in commit.record) { + expect(commit.record?.text).toEqual(text); + } + } + }); + + it('should create a commit with a custom record', () => { + const factory = JetstreamCommitFactory.factory(); + const customRecord = { text: 'custom message' } as NewSkeetRecord; // assuming simple structure + const commit = factory.record(customRecord).create(); + expect(commit.record).toEqual(customRecord); + }); + + it('should create a commit with a custom record', () => { + const factory = JetstreamCommitFactory.factory(); + const customRecord = { text: 'custom message' } as NewSkeetRecord; // assuming simple structure + const commit = factory.record(customRecord).create(); + expect(commit.record).toEqual(customRecord); + + customRecord.text = 'new message'; + factory.text('new message'); + expect(commit.record).toEqual(customRecord); + }); }); - //record - it('Updates the CreateSkeetMessageRecord with a given record', () => { - const record = CreateSkeetRecordFactory.factory().create(); - factory.record(record); - defaultCreateSkeetMessage.record = record; - expect(factory.create()).toEqual(defaultCreateSkeetMessage); + + describe('JetstreamEventFactory', () => { + it('should create a commit if none provided', () => { + const factory = JetstreamEventFactory.factory(); + factory.commit(); // This should invoke JetstreamCommitFactory.make() + const event = factory.create(); + expect(event.commit).toBeDefined(); + }); + + it('should be default for make', () => { + const event = JetstreamEventFactory.make(); + expect(event).toEqual({ + did: 'did:plc:example', + kind: 'commit', + time_us: 0, + }); + }); + }); + + describe('JetstreamIdentityFactory', () => { + it('should create identity with a custom sequence', () => { + const factory = JetstreamIdentityFactory.factory(); + const seq = 99; + const identity = factory.sequence(seq).create(); // If this method exists + expect(identity.seq).toEqual(seq); + }); + + it('should create identity with a custom sequence', () => { + const identity = JetstreamIdentityFactory.make(); + expect(identity).toEqual({ + did: 'did:plc:example', + handle: 'handle.example', + seq: 0, + time: '', + }); + }); }); }); diff --git a/tests/types/factories/RecordFactories.test.ts b/tests/types/factories/RecordFactories.test.ts index 39fd56d..a63ea92 100644 --- a/tests/types/factories/RecordFactories.test.ts +++ b/tests/types/factories/RecordFactories.test.ts @@ -1,21 +1,20 @@ import { CreateSkeetRecord, CreateSkeetRecordFactory, - Record, - RecordFactory, - Reply, + JetstreamRecord, + JetstreamRecordFactory, + JetstreamReply, + JetstreamSubject, + JetstreamSubjectFactory, ReplyFactory, - Subject, - SubjectFactory, } from '../../../src'; -import { factory } from 'ts-jest/dist/transformers/hoist-jest'; describe('RecordFactory', () => { - let factory: RecordFactory; - let defaultRecord: Record; + let factory: JetstreamRecordFactory; + let defaultRecord: JetstreamRecord; beforeEach(() => { - factory = RecordFactory.factory(); + factory = JetstreamRecordFactory.factory(); defaultRecord = { $type: 'app.bsky.feed.like', createdAt: '', @@ -23,13 +22,18 @@ describe('RecordFactory', () => { }; }); + it('default', () => { + const record = JetstreamRecordFactory.make(); + expect(record).toEqual(JetstreamRecordFactory.factory().create()); + }); + it('Type', () => { const record = factory.type('app.bsky.feed.like').create(); expect(record).toEqual(defaultRecord); }); it('subject', () => { - const subject: Subject = SubjectFactory.make(); + const subject: JetstreamSubject = JetstreamSubjectFactory.make(); const record = factory.subject(subject).create(); defaultRecord.subject = subject; expect(record).toEqual(defaultRecord); @@ -83,7 +87,7 @@ describe('CreateSkeetRecordFactory', () => { expect(record).toEqual(defaultCreateSkeetRecord); }); - it('updates the CreateSkeetRecord reply with a given Reply', () => { + it('updates the CreateSkeetRecord reply with a given JetstreamReply', () => { const reply = ReplyFactory.factory().create(); defaultCreateSkeetRecord.reply = reply; const record = factory.reply(reply).create(); @@ -93,15 +97,15 @@ describe('CreateSkeetRecordFactory', () => { }); describe('SubjectFactory', () => { - let factory: SubjectFactory; - let defaultSubject: Subject; + let factory: JetstreamSubjectFactory; + let defaultSubject: JetstreamSubject; beforeEach(() => { - factory = SubjectFactory.factory(); - defaultSubject = SubjectFactory.make(); + factory = JetstreamSubjectFactory.factory(); + defaultSubject = JetstreamSubjectFactory.make(); }); - it('creates a new Subject with factory, and a default subject with create', () => { + it('creates a new JetstreamSubject with factory, and a default subject with create', () => { const message = factory.create(); expect(message).toEqual(defaultSubject); }); @@ -122,20 +126,20 @@ describe('SubjectFactory', () => { }); describe('ReplyFactory', () => { let factory: ReplyFactory; - let defaultReply: Reply; + let defaultReply: JetstreamReply; beforeEach(() => { factory = ReplyFactory.factory(); defaultReply = ReplyFactory.make(); }); - it('creates a new ReplyFactory with factory, and a default Reply with create', () => { + it('creates a new ReplyFactory with factory, and a default JetstreamReply with create', () => { const message = factory.create(); expect(message).toEqual(defaultReply); }); - it('updates the Reply Root with a given root Subject', () => { - const root: Subject = { + it('updates the JetstreamReply Root with a given root JetstreamSubject', () => { + const root: JetstreamSubject = { cid: 'uri', uri: 'uri', }; @@ -144,8 +148,8 @@ describe('ReplyFactory', () => { expect(factory.create()).toEqual(defaultReply); }); - it('updates the Reply parent with a given parent Subject', () => { - const parent: Subject = { + it('updates the JetstreamReply parent with a given parent JetstreamSubject', () => { + const parent: JetstreamSubject = { cid: 'uri', uri: 'uri', }; @@ -154,7 +158,7 @@ describe('ReplyFactory', () => { expect(factory.create()).toEqual(defaultReply); }); - it('updates the Reply parent uri to be replying to the given did', () => { + it('updates the JetstreamReply parent uri to be replying to the given did', () => { const did: string = 'did:plc:example'; defaultReply.parent.uri = `at://${did}/app.bsky.feed.post/rkey`; factory.replyTo(did); diff --git a/tests/utils/logging-utils.test.ts b/tests/utils/logging-utils.test.ts index 94b075a..56f6a01 100644 --- a/tests/utils/logging-utils.test.ts +++ b/tests/utils/logging-utils.test.ts @@ -1,6 +1,5 @@ import { debugLog, nowDateTime } from '../../src'; import mocked = jest.mocked; -import { BskyAgent } from '@atproto/api'; jest.mock('console', () => ({ log: jest.fn(), diff --git a/tests/utils/time-utils.test.ts b/tests/utils/time-utils.test.ts index 4781a02..1beee75 100644 --- a/tests/utils/time-utils.test.ts +++ b/tests/utils/time-utils.test.ts @@ -1,5 +1,11 @@ import { advanceTo, clear } from 'jest-date-mock'; -import { getHumanReadableDateTimeStamp, nowDateTime } from '../../src'; +import { + getHumanReadableDateTimeStamp, + getTimezonesWhereItIsAGivenTime, + isTimeInHHMMFormat, + nowDateTime, +} from '../../src'; +import moment from 'moment-timezone'; describe('getHumanReadableDateTimeStamp', () => { it('should return date time string in human readable format', () => { @@ -32,3 +38,46 @@ describe('nowDateTime function', () => { expect(result).toBe('4/2/2023, 02:30 PM'); }); }); + +describe('getTimezonesWhereItIsAGivenTime', () => { + const timeScenarios = [ + { time: '12:00', timezone: 'Etc/GMT' }, + { time: '02:30', timezone: 'Asia/Kolkata' }, + { time: '19:00', timezone: 'America/New_York' }, + { time: '09:00', timezone: 'Europe/Berlin' }, + { time: '06:00', timezone: 'Africa/Cairo' }, + ]; + + timeScenarios.forEach((scenario, i) => { + it(`should return correct timezones where it is a given time - scenario ${i + 1}`, () => { + const dateWithZone = moment.tz( + scenario.time, + 'HH:mm', + scenario.timezone + ); + advanceTo(dateWithZone.toDate()); + const result = getTimezonesWhereItIsAGivenTime(scenario.time); + expect(result).toContain(scenario.timezone); + }); + }); +}); + +describe('isTimeInHHMMFormat', () => { + it('should validate "HH:mm" time format correctly', () => { + // Test data + const validTimes = ['00:00', '23:59', '02:30', '14:45']; + const invalidTimes = ['24:00', '00:60', '2:30', '14:5', 'invalid', '']; + + // Test the valid times + for (const time of validTimes) { + expect(isTimeInHHMMFormat(time)).toBe(true); + } + + // Test the invalid times + for (const time of invalidTimes) { + const res: boolean = isTimeInHHMMFormat(time); + console.log(`${res} ${time}`); + expect(res).toBe(false); + } + }); +}); diff --git a/tests/validations/BotValidators/IsBadBotValidator.test.ts b/tests/validations/BotValidators/IsBadBotValidator.test.ts index 1c0ba71..72e734d 100644 --- a/tests/validations/BotValidators/IsBadBotValidator.test.ts +++ b/tests/validations/BotValidators/IsBadBotValidator.test.ts @@ -1,12 +1,22 @@ import { - CreateSkeetMessage, - CreateSkeetMessageFactory, - CreateSkeetRecordFactory, HandlerAgent, IsBadBotValidator, + JetstreamCollectionType, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, ReplyFactory, } from '../../../src'; import { BskyAgent } from '@atproto/api'; +import fs from 'fs'; +import dotenv from 'dotenv'; + +const sessPath = './tests/temp/bot'; +fs.mkdirSync(sessPath, { recursive: true }); + +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; const botDid = 'did:plc:bot'; @@ -21,63 +31,83 @@ const mockAgent: HandlerAgent = new HandlerAgent( 'password', bskyAgent ); + +const createMessage = ( + text: string, + collection: JetstreamCollectionType, + replyToBot: boolean +) => { + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection(collection) + .record(recordFactory(text, replyToBot)) + .create() + ) + .create() as JetstreamEventCommit; +}; + +const recordFactory = (text: string, replyToBot: boolean) => { + const record = NewSkeetRecordFactory.factory().text(text); + + if (replyToBot) { + record.reply(ReplyFactory.factory().replyTo(botDid).create()); + } + + return record.create(); +}; + describe('IsBadBotValidator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + const validator = IsBadBotValidator.make(); it('shouldTrigger returns true for negative bot responses', async () => { - const negativeMessage: CreateSkeetMessage = - CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory() - .reply(ReplyFactory.factory().replyTo(botDid).create()) - .text('bad bot') - .create() - ) - .create(); - expect(await validator.shouldTrigger(negativeMessage, mockAgent)).toBe( + const negativeMessage = createMessage( + 'bad bot', + 'app.bsky.feed.post', + true + ); + expect(await validator.shouldTrigger(mockAgent, negativeMessage)).toBe( true ); }); it('shouldTrigger returns false for non-negative bot responses', async () => { - const positiveMessage: CreateSkeetMessage = - CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory() - .reply(ReplyFactory.factory().replyTo(botDid).create()) - .text('good bot') - .create() - ) - .create(); - expect(await validator.shouldTrigger(positiveMessage, mockAgent)).toBe( + const positiveMessage = createMessage( + 'goot bot', + 'app.bsky.feed.post', + true + ); + expect(await validator.shouldTrigger(mockAgent, positiveMessage)).toBe( false ); }); it('shouldTrigger returns false for non post collection', async () => { - const positiveMessage: CreateSkeetMessage = - CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory() - .reply(ReplyFactory.factory().replyTo(botDid).create()) - .text('bad bot') - .create() - ) - .collection('app.bsky.feed.like') - .create(); - expect(await validator.shouldTrigger(positiveMessage, mockAgent)).toBe( + const positiveMessage = createMessage( + 'bad bot', + 'app.bsky.feed.like', + false + ); + expect(await validator.shouldTrigger(mockAgent, positiveMessage)).toBe( false ); }); it('shouldTrigger returns false for non reply', async () => { - const negativeMessage: CreateSkeetMessage = - CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory().text('bad bot').create() - ) - .create(); - expect(await validator.shouldTrigger(negativeMessage, mockAgent)).toBe( + const negativeMessage = createMessage( + 'bad bot', + 'app.bsky.feed.post', + false + ); + expect(await validator.shouldTrigger(mockAgent, negativeMessage)).toBe( false ); }); diff --git a/tests/validations/BotValidators/IsGoodBotValidator.test.ts b/tests/validations/BotValidators/IsGoodBotValidator.test.ts index bf8f2f9..674ebad 100644 --- a/tests/validations/BotValidators/IsGoodBotValidator.test.ts +++ b/tests/validations/BotValidators/IsGoodBotValidator.test.ts @@ -1,13 +1,22 @@ import { - CreateMessageFactory, - CreateSkeetMessage, - CreateSkeetMessageFactory, - CreateSkeetRecordFactory, HandlerAgent, IsGoodBotValidator, + JetstreamCollectionType, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, ReplyFactory, } from '../../../src'; import { BskyAgent } from '@atproto/api'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/bot'; +fs.mkdirSync(sessPath, { recursive: true }); + +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; const botDid = 'did:plc:bot'; const bskyAgent: BskyAgent = { @@ -21,63 +30,69 @@ const mockAgent: HandlerAgent = new HandlerAgent( 'password', bskyAgent ); + describe('IsGoodBotValidator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + const validator = IsGoodBotValidator.make(); - const botReply = ReplyFactory.factory().replyTo(botDid).create(); - const skeetRecord = CreateSkeetRecordFactory.factory() - .text('great bot') - .reply(botReply) - .create(); + + const createMessage = ( + text: string, + reply: boolean = true, + collection: JetstreamCollectionType = 'app.bsky.feed.post' + ) => { + const recordFactory = NewSkeetRecordFactory.factory().text(text); + + if (reply) { + recordFactory.reply( + ReplyFactory.factory().replyTo(botDid).create() + ); + } + + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection(collection) + .record(recordFactory.create()) + .create() + ) + .create() as JetstreamEventCommit; + }; it('shouldTrigger returns true for positive bot responses', async () => { - const positiveMessage = CreateMessageFactory.factory() - .record(skeetRecord) - .create(); - expect(await validator.shouldTrigger(positiveMessage, mockAgent)).toBe( + const positiveMessage = createMessage('great bot'); + expect(await validator.shouldTrigger(mockAgent, positiveMessage)).toBe( true ); }); it('shouldTrigger returns true for thank you', async () => { - skeetRecord.text = 'ok thank you'; - const positiveMessage = CreateMessageFactory.factory() - .record(skeetRecord) - .create(); - expect(await validator.shouldTrigger(positiveMessage, mockAgent)).toBe( + const positiveMessage = createMessage('ok thank you'); + expect(await validator.shouldTrigger(mockAgent, positiveMessage)).toBe( true ); }); it('shouldTrigger returns false for non-positive bot responses', async () => { - skeetRecord.text = 'bad bot'; - const negativeMessage = CreateMessageFactory.factory() - .record(skeetRecord) - .create(); - expect(await validator.shouldTrigger(negativeMessage, mockAgent)).toBe( + const negativeMessage = createMessage('bad bot'); + expect(await validator.shouldTrigger(mockAgent, negativeMessage)).toBe( false ); }); it('shouldTrigger returns false for non post collection', async () => { - const positiveMessage: CreateSkeetMessage = - CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory().text('bad bot').create() - ) - .collection('app.bsky.feed.like') - .create(); - expect(await validator.shouldTrigger(positiveMessage, mockAgent)).toBe( - false - ); + const message = createMessage('bad bot', true, 'app.bsky.feed.like'); + expect(await validator.shouldTrigger(mockAgent, message)).toBe(false); }); it('shouldTrigger returns false for non reply', async () => { - skeetRecord.reply = undefined; - const positiveMessage = CreateMessageFactory.factory() - .record(skeetRecord) - .create(); - expect(await validator.shouldTrigger(positiveMessage, mockAgent)).toBe( - false - ); + const message = createMessage('great bot', false); + expect(await validator.shouldTrigger(mockAgent, message)).toBe(false); }); }); diff --git a/tests/validations/GenericValidators/ActionTakenByUserValidators.test.ts b/tests/validations/GenericValidators/ActionTakenByUserValidators.test.ts index 804bd6e..b3c910b 100644 --- a/tests/validations/GenericValidators/ActionTakenByUserValidators.test.ts +++ b/tests/validations/GenericValidators/ActionTakenByUserValidators.test.ts @@ -1,10 +1,11 @@ import { ActionTakenByUserValidator, - CreateSkeetMessage, - CreateSkeetMessageFactory, HandlerAgent, - JetstreamMessage, - JetstreamMessageFactory, + JetstreamCollectionType, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, } from '../../../src'; describe('Action Taken By User', () => { @@ -12,28 +13,36 @@ describe('Action Taken By User', () => { const validator = ActionTakenByUserValidator.make(validDid); const handlerAgent: HandlerAgent = {} as HandlerAgent; - it('shouldTrigger returns true if posted by same did', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .fromDid(validDid) - .create(); + const createMessage = ( + did: string = 'did::plc:default', + collection: JetstreamCollectionType = 'app.bsky.feed.post' + ) => { + return JetstreamEventFactory.factory() + .fromDid(did) + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection(collection) + .record(NewSkeetRecordFactory.factory().create()) + .create() + ) + .create() as JetstreamEventCommit; + }; - expect(await validator.shouldTrigger(message, handlerAgent)).toBe(true); + it('shouldTrigger returns true if posted by same did', async () => { + const message = createMessage(validDid); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe(true); }); it('shouldTrigger returns false not posted by same user', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.make(); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe( + const message = createMessage(); // Assuming this line can remain as is for a basic test message without a specific did + expect(await validator.shouldTrigger(handlerAgent, message)).toBe( false ); }); it('shouldTrigger returns true if not a post, and posted by user', async () => { - const message: JetstreamMessage = JetstreamMessageFactory.factory() - .fromDid(validDid) - .collection('app.bsky.feed.like') - .create(); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe(true); + const message = createMessage(validDid, 'app.bsky.feed.like'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe(true); }); }); diff --git a/tests/validations/LogicalValidators/NotValidator.test.ts b/tests/validations/LogicalValidators/NotValidator.test.ts index e32559c..3766d5b 100644 --- a/tests/validations/LogicalValidators/NotValidator.test.ts +++ b/tests/validations/LogicalValidators/NotValidator.test.ts @@ -1,39 +1,81 @@ import { AbstractValidator, HandlerAgent, - JetstreamMessage, - JetstreamMessageFactory, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, TestValidator, } from '../../../src'; +import { AbstractMessageValidator } from '../../../src/validations/message-validators/AbstractMessageValidator'; +import { TestMessageValidator } from '../../../src/validations/message-validators/TestMessageValidator'; + +const createMessage = () => { + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record(NewSkeetRecordFactory.factory().create()) + .create() + ) + .create() as JetstreamEventCommit; +}; describe('Testing Negating', () => { const handlerAgent: HandlerAgent = {} as HandlerAgent; - const message: JetstreamMessage = JetstreamMessageFactory.make(); + + const message = createMessage(); test('shouldTrigger returns false if given validator is true', async () => { const testValidator: AbstractValidator = TestValidator.make(true).not(); - - expect(await testValidator.shouldTrigger(message, handlerAgent)).toBe( + expect(await testValidator.shouldTrigger(handlerAgent, message)).toBe( false ); }); test('shouldTrigger returns true if given validator is false', async () => { - const testValidator: AbstractValidator = TestValidator.make(true).not(); + const testValidator: AbstractValidator = + TestValidator.make(false).not(); + expect(await testValidator.shouldTrigger(handlerAgent, message)).toBe( + true + ); + }); +}); + +describe('Testing message Negating', () => { + const handlerAgent: HandlerAgent = {} as HandlerAgent; + const message = createMessage(); - expect(await testValidator.shouldTrigger(message, handlerAgent)).toBe( + test('shouldTrigger returns false if given validator is true', async () => { + const testValidator: AbstractMessageValidator = + TestMessageValidator.make(true).not(); + expect(await testValidator.shouldTrigger(handlerAgent, message)).toBe( false ); }); + + test('shouldTrigger returns true if given validator is false', async () => { + const testValidator: AbstractMessageValidator = + TestMessageValidator.make(false).not(); + expect(await testValidator.shouldTrigger(handlerAgent, message)).toBe( + true + ); + }); }); describe('Test AbstractValidatorError', () => { - const handlerAgent: HandlerAgent = {} as HandlerAgent; - const message: JetstreamMessage = JetstreamMessageFactory.make(); - test('make throws error on abstract', async () => { expect(AbstractValidator.make).toThrow( 'Method Not Implemented! Use constructor.' ); }); }); + +describe('Test AbstractMessageValidatorError', () => { + test('make throws error on abstract', async () => { + expect(AbstractMessageValidator.make).toThrow( + 'Method Not Implemented! Use constructor.' + ); + }); +}); diff --git a/tests/validations/LogicalValidators/OrValidator.test.ts b/tests/validations/LogicalValidators/OrValidator.test.ts index 58f1b43..4661cc7 100644 --- a/tests/validations/LogicalValidators/OrValidator.test.ts +++ b/tests/validations/LogicalValidators/OrValidator.test.ts @@ -1,10 +1,11 @@ import { - CreateSkeetMessage, - CreateSkeetMessageFactory, - CreateSkeetRecordFactory, HandlerAgent, InputEqualsValidator, InputStartsWithValidator, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, OrValidator, } from '../../../src'; @@ -18,31 +19,35 @@ describe('OrValidator', () => { ]); const handlerAgent: HandlerAgent = {} as HandlerAgent; + const createMessage = (text: string) => { + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record(NewSkeetRecordFactory.factory().text(text).create()) + .create() + ) + .create() as JetstreamEventCommit; + }; + test('shouldTrigger returns true if both validators pass', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .record(CreateSkeetRecordFactory.factory().text('test').create()) - .create(); - expect(await orValidator.shouldTrigger(message, handlerAgent)).toBe( + const message = createMessage('test'); + expect(await orValidator.shouldTrigger(handlerAgent, message)).toBe( true ); }); test('shouldTrigger returns true if one validator passes', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory().text('test message').create() - ) - .create(); - expect(await orValidator.shouldTrigger(message, handlerAgent)).toBe( + const message = createMessage('test message'); + expect(await orValidator.shouldTrigger(handlerAgent, message)).toBe( true ); }); test('shouldTrigger returns false if no validators pass', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .record(CreateSkeetRecordFactory.factory().text('random').create()) - .create(); - expect(await orValidator.shouldTrigger(message, handlerAgent)).toBe( + const message = createMessage('random'); + expect(await orValidator.shouldTrigger(handlerAgent, message)).toBe( false ); }); diff --git a/tests/validations/LogicalValidators/SimpleFunctionValidator.test.ts b/tests/validations/LogicalValidators/SimpleFunctionValidator.test.ts index 46b051e..320243e 100644 --- a/tests/validations/LogicalValidators/SimpleFunctionValidator.test.ts +++ b/tests/validations/LogicalValidators/SimpleFunctionValidator.test.ts @@ -1,14 +1,15 @@ import { HandlerAgent, - JetstreamMessage, - JetstreamMessageFactory, + JetstreamEventCommit, + JetstreamEventFactory, SimpleFunctionValidator, } from '../../../src'; describe('FunctionAction', () => { const mockHandlerAgent = {} as HandlerAgent; - const mockMessage: JetstreamMessage = JetstreamMessageFactory.make(); + const mockMessage: JetstreamEventCommit = + JetstreamEventFactory.factory().create() as JetstreamEventCommit; let mockvalidatorFunction = jest.fn(); let functionValidator: SimpleFunctionValidator; @@ -24,12 +25,12 @@ describe('FunctionAction', () => { mockvalidatorFunction ); await functionValidator.shouldTrigger( - mockMessage, - mockHandlerAgent + mockHandlerAgent, + mockMessage ); expect(mockvalidatorFunction).toHaveBeenCalledWith( - mockMessage, - mockHandlerAgent + mockHandlerAgent, + mockMessage ); }); }); diff --git a/tests/validations/TestMessageValidator.test.ts b/tests/validations/TestMessageValidator.test.ts new file mode 100644 index 0000000..d51efce --- /dev/null +++ b/tests/validations/TestMessageValidator.test.ts @@ -0,0 +1,36 @@ +import { + HandlerAgent, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, +} from '../../src'; +import { TestMessageValidator } from '../../src/validations/message-validators/TestMessageValidator'; + +const mockAgent: HandlerAgent = {} as HandlerAgent; +describe('TestMessageValidator', () => { + test('shouldTrigger returns true for true attribute', async () => { + const validator = TestMessageValidator.make(true); + const msg: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('delete') + .collection('app.bsky.feed.post') + .create() + ) + .create() as JetstreamEventCommit; + expect(await validator.shouldTrigger(mockAgent, msg)).toBe(true); + }); + + test('shouldTrigger returns false for false attribute', async () => { + const validator = TestMessageValidator.make(false); + const msg: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('delete') + .collection('app.bsky.feed.post') + .create() + ) + .create() as JetstreamEventCommit; + expect(await validator.shouldTrigger(mockAgent, msg)).toBe(false); + }); +}); diff --git a/tests/validations/TestValidator.test.ts b/tests/validations/TestValidator.test.ts index dbe4ad3..45950e1 100644 --- a/tests/validations/TestValidator.test.ts +++ b/tests/validations/TestValidator.test.ts @@ -1,36 +1,14 @@ -import { - CreateSkeetMessage, - HandlerAgent, - IsGoodBotValidator, - Subject, - CreateSkeetMessageFactory, - CreateSkeetRecordFactory, -} from '../../src'; -import { TestValidator } from '../../src'; +import { HandlerAgent, TestValidator } from '../../src'; const mockAgent: HandlerAgent = {} as HandlerAgent; describe('TestValidator', () => { test('shouldTrigger returns true for true attribute', async () => { const validator = TestValidator.make(true); - const positiveMessage = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory().text('great bot').create() - ) - .create(); - expect(await validator.shouldTrigger(positiveMessage, mockAgent)).toBe( - true - ); + expect(await validator.shouldTrigger(mockAgent)).toBe(true); }); test('shouldTrigger returns false for false attribute', async () => { const validator = TestValidator.make(false); - const positiveMessage = CreateSkeetMessageFactory.factory() - .record( - CreateSkeetRecordFactory.factory().text('great bot').create() - ) - .create(); - expect(await validator.shouldTrigger(positiveMessage, mockAgent)).toBe( - false - ); + expect(await validator.shouldTrigger(mockAgent)).toBe(false); }); }); diff --git a/tests/validations/follow/NewFollowFromUserValidator.test.ts b/tests/validations/follow/NewFollowFromUserValidator.test.ts deleted file mode 100644 index 87ca0fd..0000000 --- a/tests/validations/follow/NewFollowFromUserValidator.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - CreateMessage, - CreateMessageFactory, - HandlerAgent, - NewFollowFromUserValidator, - RecordFactory, - UserFollowedValidator, -} from '../../../src'; -import { BskyAgent } from '@atproto/api'; - -describe('New Follow From User Validator', () => { - const botDid = 'did:plc:bot'; - const testDid = 'did:plc:test'; - const bskyAgent: BskyAgent = { - session: { - did: botDid, - }, - } as BskyAgent; - const mockHandlerAgent: HandlerAgent = new HandlerAgent( - 'name', - 'handle', - 'password', - bskyAgent - ); - - it('shouldTrigger returns true if no did provided, and follow is by bot user', async () => { - const validator = NewFollowFromUserValidator.make(); - const message: CreateMessage = CreateMessageFactory.factory() - .fromDid(botDid) - .record(RecordFactory.factory().isFollow().create()) - .create(); - - expect(await validator.shouldTrigger(message, mockHandlerAgent)).toBe( - true - ); - }); - - it('shouldTrigger returns true if given did is same as message did', async () => { - const validator = NewFollowFromUserValidator.make(testDid); - const message: CreateMessage = CreateMessageFactory.factory() - .fromDid(testDid) - .record(RecordFactory.factory().isFollow().create()) - .create(); - - expect(await validator.shouldTrigger(message, mockHandlerAgent)).toBe( - true - ); - }); - - it('shouldTrigger returns false if given did is different from message did', async () => { - const validator = NewFollowFromUserValidator.make('did:plc:test'); - const message: CreateMessage = CreateMessageFactory.factory() - .fromDid(botDid) - .record(RecordFactory.factory().isFollow().create()) - .create(); - - expect(await validator.shouldTrigger(message, mockHandlerAgent)).toBe( - false - ); - }); - - it('shouldTrigger returns false if default bot did not follow did', async () => { - const validator = NewFollowFromUserValidator.make(); - const message: CreateMessage = CreateMessageFactory.factory() - .fromDid(testDid) - .record(RecordFactory.factory().isFollow().create()) - .create(); - - expect(await validator.shouldTrigger(message, mockHandlerAgent)).toBe( - false - ); - }); - - it('shouldTrigger returns true if using deprecated UserFollowedValidator', async () => { - const validator = UserFollowedValidator.make(); - const message: CreateMessage = CreateMessageFactory.factory() - .fromDid(botDid) - .record(RecordFactory.factory().isFollow().create()) - .create(); - - expect(await validator.shouldTrigger(message, mockHandlerAgent)).toBe( - true - ); - }); - - it('shouldTrigger returns false if using deprecated UserFollowedValidator and given did differs', async () => { - const validator = UserFollowedValidator.make(testDid); - const message: CreateMessage = CreateMessageFactory.factory() - .fromDid(botDid) - .record(RecordFactory.factory().isFollow().create()) - .create(); - - expect(await validator.shouldTrigger(message, mockHandlerAgent)).toBe( - false - ); - }); -}); diff --git a/tests/validations/follow/NewFollowerForUserValidator.test.ts b/tests/validations/follow/NewFollowerForUserValidator.test.ts deleted file mode 100644 index ff349dc..0000000 --- a/tests/validations/follow/NewFollowerForUserValidator.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - CreateMessage, - CreateMessageFactory, - HandlerAgent, - NewFollowerForUserValidator, - RecordFactory, -} from '../../../src'; -import { BskyAgent } from '@atproto/api'; - -describe('New Follower For User Validator', () => { - const botDid = 'did:plc:bot'; - const bskyAgent: BskyAgent = { - session: { - did: botDid, - }, - } as BskyAgent; - const mockHandlerAgent: HandlerAgent = new HandlerAgent( - 'name', - 'handle', - 'password', - bskyAgent - ); - - it('shouldTrigger returns true if no did provided, and follow is by bot user', async () => { - const validator = NewFollowerForUserValidator.make(); - const message: CreateMessage = CreateMessageFactory.factory() - .record(RecordFactory.factory().isFollow(botDid).create()) - .create(); - - expect(await validator.shouldTrigger(message, mockHandlerAgent)).toBe( - true - ); - }); - - it('shouldTrigger returns true if given did is same as message did', async () => { - const testDid = 'did:plc:test'; - const validator = NewFollowerForUserValidator.make(testDid); - const message: CreateMessage = CreateMessageFactory.factory() - .record(RecordFactory.factory().isFollow(testDid).create()) - .create(); - expect(await validator.shouldTrigger(message, mockHandlerAgent)).toBe( - true - ); - }); - - it('shouldTrigger returns false if given did is different from message did', async () => { - const testDid = 'did:plc:test'; - const validator = NewFollowerForUserValidator.make(testDid); - const message: CreateMessage = CreateMessageFactory.factory() - .record(RecordFactory.factory().isFollow('did:plc:other').create()) - .create(); - expect(await validator.shouldTrigger(message, mockHandlerAgent)).toBe( - false - ); - }); - - it('shouldTrigger returns false if default bot did not get followed', async () => { - const testDid = 'did:plc:test'; - const validator = NewFollowerForUserValidator.make(); - const message: CreateMessage = CreateMessageFactory.factory() - .record(RecordFactory.factory().isFollow(testDid).create()) - .create(); - - expect(await validator.shouldTrigger(message, mockHandlerAgent)).toBe( - false - ); - }); -}); diff --git a/tests/validations/interval-validators/IsFourTwentyValidator.test.ts b/tests/validations/interval-validators/IsFourTwentyValidator.test.ts new file mode 100644 index 0000000..8bdf9fe --- /dev/null +++ b/tests/validations/interval-validators/IsFourTwentyValidator.test.ts @@ -0,0 +1,57 @@ +import { advanceTo, clear } from 'jest-date-mock'; +import { HandlerAgent, IsFourTwentyValidator } from '../../../src'; + +const mockAgent: HandlerAgent = {} as HandlerAgent; + +describe('IsFourTwentyValidator', () => { + afterAll(() => { + clear(); // Clear the mock + }); + test('shouldTrigger returns true if it is 4:20 AM/PM somewhere', async () => { + advanceTo(new Date('2024-01-16T16:20:00')); + const validator = IsFourTwentyValidator.make(); + expect(await validator.shouldTrigger(mockAgent)).toBe(true); + }); + + test('shouldTrigger returns true if it is 4:20 AM/PM somewhere', async () => { + advanceTo(new Date('2024-01-16T14:20:00')); + const validator = IsFourTwentyValidator.make(); + expect(await validator.shouldTrigger(mockAgent)).toBe(true); + }); + + test('shouldTrigger returns false if it is not 4:20 AM/PM anywhere', async () => { + // Advance to a time where it's not 4:20 anywhere in the world + advanceTo(new Date('2024-01-16T14:00:00')); + const validator = IsFourTwentyValidator.make(); + expect(await validator.shouldTrigger(mockAgent)).toBe(false); + }); +}); + +describe('IsFourTwentyValidator getTimezonesWhereItIsFourTwenty', () => { + afterAll(() => { + clear(); // Clear the mock + }); + test('shouldTrigger returns true if it is 4:20 AM/PM somewhere', async () => { + advanceTo(new Date('2024-01-16T16:20:00')); + const timezones = + IsFourTwentyValidator.getTimezonesWhereItIsFourTwenty(); + console.log(timezones); + expect(timezones.totalTimezones).toBe(47); + }); + + test('shouldTrigger returns true if it is 4:20 AM/PM somewhere', async () => { + advanceTo(new Date('2024-01-16T15:20:00')); + const timezones = + IsFourTwentyValidator.getTimezonesWhereItIsFourTwenty(); + console.log(timezones); + expect(timezones.totalTimezones).toBe(63); + }); + + test('shouldTrigger returns true if it is 4:20 AM/PM somewhere', async () => { + advanceTo(new Date('2024-01-16T14:20:00')); + const timezones = + IsFourTwentyValidator.getTimezonesWhereItIsFourTwenty(); + console.log(timezones); + expect(timezones.totalTimezones).toBe(68); + }); +}); diff --git a/tests/validations/interval-validators/IsSpecifiedTimeValidator.test.ts b/tests/validations/interval-validators/IsSpecifiedTimeValidator.test.ts new file mode 100644 index 0000000..3b646fa --- /dev/null +++ b/tests/validations/interval-validators/IsSpecifiedTimeValidator.test.ts @@ -0,0 +1,47 @@ +import { advanceTo, clear } from 'jest-date-mock'; +import { DebugLog, HandlerAgent, IsSpecifiedTimeValidator } from '../../../src'; + +const mockAgent: HandlerAgent = {} as HandlerAgent; + +describe('IsSpecifiedTimeValidator', () => { + afterAll(() => { + clear(); // Clear the mock + }); + test('shouldTrigger returns true if it is 4:20 AM somewhere', async () => { + advanceTo(new Date('2024-01-16T16:20:00')); + const validator = IsSpecifiedTimeValidator.make('04:20'); + expect(await validator.shouldTrigger(mockAgent)).toBe(true); + }); + + test('shouldTrigger returns true if it is 6:20 AM somewhere', async () => { + advanceTo(new Date('2024-01-16T14:20:00')); + const validator = IsSpecifiedTimeValidator.make('06:20'); + expect(await validator.shouldTrigger(mockAgent)).toBe(true); + }); + + test('shouldTrigger returns true if it is 4:20 AM/PM somewhere', async () => { + advanceTo(new Date('2024-01-16T14:20:00')); + const validator = IsSpecifiedTimeValidator.make('04:20', '16:20'); + expect(await validator.shouldTrigger(mockAgent)).toBe(true); + }); + + test('shouldTrigger returns false if it is not 6:20 AM anywhere', async () => { + // Advance to a time where it's not 4:20 anywhere in the world + advanceTo(new Date('2024-01-16T14:21:00')); + const validator = IsSpecifiedTimeValidator.make('06:20'); + expect(await validator.shouldTrigger(mockAgent)).toBe(false); + }); + + test('shouldTrigger returns false if it is not 6:20 AM anywhere', async () => { + // Advance to a time where it's not 4:20 anywhere in the world + advanceTo(new Date('2024-01-16T14:21:00')); + const mockError = jest.fn(); + DebugLog.error = mockError; + const validator = IsSpecifiedTimeValidator.make('invalid'); + expect(await validator.shouldTrigger(mockAgent)).toBe(false); + expect(mockError).toHaveBeenCalledWith( + 'Time Validator', + 'invalid is not in a valid format' + ); + }); +}); diff --git a/tests/validations/message-validators/follow/NewFollowFromUserValidator.test.ts b/tests/validations/message-validators/follow/NewFollowFromUserValidator.test.ts new file mode 100644 index 0000000..f661816 --- /dev/null +++ b/tests/validations/message-validators/follow/NewFollowFromUserValidator.test.ts @@ -0,0 +1,148 @@ +import { + HandlerAgent, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + JetstreamRecordFactory, + NewFollowFromUserValidator, + UserFollowedValidator, +} from '../../../../src'; +import { BskyAgent } from '@atproto/api'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/follow/NewFollow'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('New Follow From User Validator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + const botDid = 'did:plc:bot'; + const testDid = 'did:plc:test'; + const bskyAgent: BskyAgent = { + session: { + did: botDid, + }, + } as BskyAgent; + const mockHandlerAgent: HandlerAgent = new HandlerAgent( + 'name', + 'handle', + 'password', + bskyAgent + ); + + it('shouldTrigger returns true if no did provided, and follow is by bot user', async () => { + const validator = NewFollowFromUserValidator.make(); + const message: JetstreamEventCommit = JetstreamEventFactory.factory() + .fromDid(botDid) + .commit( + JetstreamCommitFactory.factory() + .record( + JetstreamRecordFactory.factory().isFollow().create() + ) + .create() + ) + .create() as JetstreamEventCommit; + + expect(await validator.shouldTrigger(mockHandlerAgent, message)).toBe( + true + ); + }); + + it('shouldTrigger returns true if given did is same as message did', async () => { + const validator = NewFollowFromUserValidator.make(testDid); + const message: JetstreamEventCommit = JetstreamEventFactory.factory() + .fromDid(testDid) + .commit( + JetstreamCommitFactory.factory() + .record( + JetstreamRecordFactory.factory().isFollow().create() + ) + .create() + ) + .create() as JetstreamEventCommit; + + expect(await validator.shouldTrigger(mockHandlerAgent, message)).toBe( + true + ); + }); + + it('shouldTrigger returns false if given did is different from message did', async () => { + const validator = NewFollowFromUserValidator.make('did:plc:test'); + const message: JetstreamEventCommit = JetstreamEventFactory.factory() + .fromDid(botDid) + .commit( + JetstreamCommitFactory.factory() + .record( + JetstreamRecordFactory.factory().isFollow().create() + ) + .create() + ) + .create() as JetstreamEventCommit; + + expect(await validator.shouldTrigger(mockHandlerAgent, message)).toBe( + false + ); + }); + + it('shouldTrigger returns false if default bot did not follow did', async () => { + const validator = NewFollowFromUserValidator.make(); + const message: JetstreamEventCommit = JetstreamEventFactory.factory() + .fromDid(testDid) + .commit( + JetstreamCommitFactory.factory() + .record( + JetstreamRecordFactory.factory().isFollow().create() + ) + .create() + ) + .create() as JetstreamEventCommit; + + expect(await validator.shouldTrigger(mockHandlerAgent, message)).toBe( + false + ); + }); + + it('shouldTrigger returns true if using deprecated UserFollowedValidator', async () => { + const validator = UserFollowedValidator.make(); + const message: JetstreamEventCommit = JetstreamEventFactory.factory() + .fromDid(botDid) + .commit( + JetstreamCommitFactory.factory() + .record( + JetstreamRecordFactory.factory().isFollow().create() + ) + .create() + ) + .create() as JetstreamEventCommit; + + expect(await validator.shouldTrigger(mockHandlerAgent, message)).toBe( + true + ); + }); + + it('shouldTrigger returns false if using deprecated UserFollowedValidator and given did differs', async () => { + const validator = UserFollowedValidator.make(testDid); + + const message: JetstreamEventCommit = JetstreamEventFactory.factory() + .fromDid(botDid) + .commit( + JetstreamCommitFactory.factory() + .record( + JetstreamRecordFactory.factory().isFollow().create() + ) + .create() + ) + .create() as JetstreamEventCommit; + + expect(await validator.shouldTrigger(mockHandlerAgent, message)).toBe( + false + ); + }); +}); diff --git a/tests/validations/message-validators/follow/NewFollowerForUserValidator.test.ts b/tests/validations/message-validators/follow/NewFollowerForUserValidator.test.ts new file mode 100644 index 0000000..448838d --- /dev/null +++ b/tests/validations/message-validators/follow/NewFollowerForUserValidator.test.ts @@ -0,0 +1,94 @@ +import { + HandlerAgent, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + JetstreamRecordFactory, + NewFollowerForUserValidator, +} from '../../../../src'; +import { BskyAgent } from '@atproto/api'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/follow/NewFollower'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('New Follower For User Validator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + + const botDid = 'did:plc:bot'; + const bskyAgent: BskyAgent = { + session: { + did: botDid, + }, + } as BskyAgent; + + const mockHandlerAgent: HandlerAgent = new HandlerAgent( + 'name', + 'handle', + 'password', + bskyAgent + ); + + const createMessage = (followDid: string) => { + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.graph.follow') + .record( + JetstreamRecordFactory.factory() + .isFollow(followDid) + .create() + ) + .create() + ) + .create() as JetstreamEventCommit; + }; + + it('shouldTrigger returns true if no did provided, and follow is by bot user', async () => { + const validator = NewFollowerForUserValidator.make(); + const message = createMessage(botDid); + + expect(await validator.shouldTrigger(mockHandlerAgent, message)).toBe( + true + ); + }); + + it('shouldTrigger returns true if given did is same as message did', async () => { + const testDid = 'did:plc:test'; + const validator = NewFollowerForUserValidator.make(testDid); + const message = createMessage(testDid); + + expect(await validator.shouldTrigger(mockHandlerAgent, message)).toBe( + true + ); + }); + + it('shouldTrigger returns false if given did is different from message did', async () => { + const testDid = 'did:plc:test'; + const validator = NewFollowerForUserValidator.make(testDid); + const message = createMessage('did:plc:other'); + + expect(await validator.shouldTrigger(mockHandlerAgent, message)).toBe( + false + ); + }); + + it('shouldTrigger returns false if default bot did not get followed', async () => { + const testDid = 'did:plc:test'; + const validator = NewFollowerForUserValidator.make(); + const message = createMessage(testDid); + + expect(await validator.shouldTrigger(mockHandlerAgent, message)).toBe( + false + ); + }); +}); diff --git a/tests/validations/message-validators/like/LikeCount/PostLikesValidator.test.ts b/tests/validations/message-validators/like/LikeCount/PostLikesValidator.test.ts new file mode 100644 index 0000000..44f6138 --- /dev/null +++ b/tests/validations/message-validators/like/LikeCount/PostLikesValidator.test.ts @@ -0,0 +1,98 @@ +import { + HandlerAgent, + JetstreamEventCommit, + PostLikesValidator, +} from '../../../../../src'; + +describe('PostLikesValidator', () => { + let mockHandlerAgent: HandlerAgent; + const postUri = 'example:uri'; + + beforeEach(() => { + mockHandlerAgent = { + getPostLikeCount: jest.fn().mockResolvedValue(5), + } as unknown as HandlerAgent; + }); + + it('should return true for equal comparison when likes match', async () => { + const validator = PostLikesValidator.make(postUri, 'equal', 5); + expect( + await validator.handle(mockHandlerAgent, {} as JetstreamEventCommit) + ).toBe(true); + }); + + it('should return false for equal comparison when likes do not match', async () => { + const validator = PostLikesValidator.make(postUri, 'equal', 10); + expect( + await validator.handle(mockHandlerAgent, {} as JetstreamEventCommit) + ).toBe(false); + }); + + it('should return true for greaterThan comparison when likes are greater', async () => { + const validator = PostLikesValidator.make(postUri, 'greaterThan', 3); + expect( + await validator.handle(mockHandlerAgent, {} as JetstreamEventCommit) + ).toBe(true); + }); + + it('should return false for greaterThan comparison when likes are not greater', async () => { + const validator = PostLikesValidator.make(postUri, 'greaterThan', 5); + expect( + await validator.handle(mockHandlerAgent, {} as JetstreamEventCommit) + ).toBe(false); + }); + + it('should return true for lessThan comparison when likes are less', async () => { + const validator = PostLikesValidator.make(postUri, 'lessThan', 10); + expect( + await validator.handle(mockHandlerAgent, {} as JetstreamEventCommit) + ).toBe(true); + }); + + it('should return false for lessThan comparison when likes are not less', async () => { + const validator = PostLikesValidator.make(postUri, 'lessThan', 5); + expect( + await validator.handle(mockHandlerAgent, {} as JetstreamEventCommit) + ).toBe(false); + }); + + it('should return true for between comparison when likes are within range', async () => { + const validator = PostLikesValidator.make( + postUri, + 'between', + undefined, + 3, + 7 + ); + expect( + await validator.handle(mockHandlerAgent, {} as JetstreamEventCommit) + ).toBe(true); + }); + + it('should return false for between comparison when likes are not within range', async () => { + const validator = PostLikesValidator.make( + postUri, + 'between', + undefined, + 6, + 10 + ); + expect( + await validator.handle(mockHandlerAgent, {} as JetstreamEventCommit) + ).toBe(false); + }); + + it('should throw an error if likeCount is missing for non-between comparison', () => { + expect(() => { + PostLikesValidator.make(postUri, 'equal'); + }).toThrow('likeCount is required for non-between comparisons'); + }); + + it('should throw an error if likeCountMin or likeCountMax is missing for between comparison', () => { + expect(() => { + PostLikesValidator.make(postUri, 'between', undefined, 5); + }).toThrow( + 'likeCountMin and likeCountMax are required for between comparisons' + ); + }); +}); diff --git a/tests/validations/message-validators/like/LikeUser/LikeByUser.test.ts b/tests/validations/message-validators/like/LikeUser/LikeByUser.test.ts new file mode 100644 index 0000000..c11dd9e --- /dev/null +++ b/tests/validations/message-validators/like/LikeUser/LikeByUser.test.ts @@ -0,0 +1,94 @@ +import { + HandlerAgent, + JetstreamEventCommit, + JetstreamCommitFactory, + JetstreamEventFactory, + JetstreamRecordFactory, + LikeByUser, +} from '../../../../../src'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/like/LikeByUser'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('Like By User Validator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + + const mockHandlerAgent: HandlerAgent = { + getDid: 'did:plc:bot', + getDIDFromUri: jest.fn().mockReturnValue('did:plc:bot'), + } as unknown as HandlerAgent; + + const createMessage = (subjectUri: string, did: string) => { + return JetstreamEventFactory.factory() + .fromDid(did) + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.like') + .record( + JetstreamRecordFactory.factory() + .subject({ uri: subjectUri, cid: 'test' }) + .create() + ) + .create() + ) + .create() as JetstreamEventCommit; + }; + + it('handle returns true if userDid is undefined and message did matches handlerAgent did', async () => { + const validator = LikeByUser.make(undefined, 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(true); + }); + + it('handle returns true if provided userDid matches message did', async () => { + const validator = LikeByUser.make('did:plc:bot', 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(true); + }); + + it('handle returns false if message commit record subject is a string', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = 'string' as unknown as string; + + const validator = LikeByUser.make(); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if message commit record subject is undefined', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = undefined; + + const validator = LikeByUser.make(); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if postUri does not match', async () => { + const validator = LikeByUser.make('did:plc:bot', 'different:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if userDid is provided and does not match message did', async () => { + const validator = LikeByUser.make('did:plc:other', 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); +}); diff --git a/tests/validations/message-validators/like/LikeUser/LikeOfPost.test.ts b/tests/validations/message-validators/like/LikeUser/LikeOfPost.test.ts new file mode 100644 index 0000000..aa535b2 --- /dev/null +++ b/tests/validations/message-validators/like/LikeUser/LikeOfPost.test.ts @@ -0,0 +1,80 @@ +import { + JetstreamEventCommit, + JetstreamCommitFactory, + JetstreamEventFactory, + JetstreamRecordFactory, + LikeOfPost, + HandlerAgent, +} from '../../../../../src'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/like/LikeOfPost'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('Like Of Post Validator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + + const mockHandlerAgent: HandlerAgent = { + getDid: 'did:plc:bot', + getDIDFromUri: jest.fn().mockReturnValue('did:plc:bot'), + } as unknown as HandlerAgent; + + const createMessage = (subjectUri: string, did: string) => { + return JetstreamEventFactory.factory() + .fromDid(did) + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.like') + .record( + JetstreamRecordFactory.factory() + .subject({ uri: subjectUri, cid: 'test' }) + .create() + ) + .create() + ) + .create() as JetstreamEventCommit; + }; + + it('handle returns true if postUri matches the message subject uri', async () => { + const validator = LikeOfPost.make('example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(true); + }); + + it('handle returns false if message commit record subject is a string', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = '' as unknown as { uri: string }; + + const validator = LikeOfPost.make('example:uri'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if message commit record subject is undefined', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = undefined; + + const validator = LikeOfPost.make('example:uri'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if postUri does not match the message subject uri', async () => { + const validator = LikeOfPost.make('different:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); +}); diff --git a/tests/validations/message-validators/like/LikeUser/LikeOfUser.test.ts b/tests/validations/message-validators/like/LikeUser/LikeOfUser.test.ts new file mode 100644 index 0000000..d7f1caf --- /dev/null +++ b/tests/validations/message-validators/like/LikeUser/LikeOfUser.test.ts @@ -0,0 +1,94 @@ +import { + HandlerAgent, + JetstreamEventCommit, + JetstreamCommitFactory, + JetstreamEventFactory, + JetstreamRecordFactory, + LikeOfUser, +} from '../../../../../src'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/repost/LikeOfUser'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('Like Of User Validator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + + const mockHandlerAgent: HandlerAgent = { + getDid: 'did:plc:bot', + getDIDFromUri: jest.fn().mockReturnValue('did:plc:bot'), + } as unknown as HandlerAgent; + + const createMessage = (subjectUri: string, did: string) => { + return JetstreamEventFactory.factory() + .fromDid(did) + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.like') + .record( + JetstreamRecordFactory.factory() + .subject({ uri: subjectUri, cid: 'test' }) + .create() + ) + .create() + ) + .create() as JetstreamEventCommit; + }; + + it('handle returns true if userDid is undefined and like is by same user', async () => { + const validator = LikeOfUser.make(undefined, 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(true); + }); + + it('handle returns true if provided userDid matches likeed post URI DID', async () => { + const validator = LikeOfUser.make('did:plc:bot', 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(true); + }); + + it('handle returns false if message commit record subject is a string', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = 'string'; + + const validator = LikeOfUser.make(); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if message commit record subject is a undefined', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = undefined; + + const validator = LikeOfUser.make(); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if postUri does not match', async () => { + const validator = LikeOfUser.make('did:plc:bot', 'different:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if userDid is provided and does not match likeed post URI DID', async () => { + const validator = LikeOfUser.make('did:plc:other', 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); +}); diff --git a/tests/validations/message-validators/post/PostValidators/IsNewPost.test.ts b/tests/validations/message-validators/post/PostValidators/IsNewPost.test.ts new file mode 100644 index 0000000..0b1197d --- /dev/null +++ b/tests/validations/message-validators/post/PostValidators/IsNewPost.test.ts @@ -0,0 +1,82 @@ +import { + HandlerAgent, + IsNewPost, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecord, +} from '../../../../../src'; +import { BskyAgent } from '@atproto/api'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/post/isNewPost'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('IsNewPost', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + const validator = IsNewPost.make(); + const botDid = 'did:plc:bot'; + const bskyAgent: BskyAgent = { + session: { + did: botDid, + }, + } as BskyAgent; + const handlerAgent: HandlerAgent = new HandlerAgent( + 'name', + 'handle', + 'password', + bskyAgent + ); + + test('handle returns false with no record ', async () => { + const recentDate = new Date(); + recentDate.setHours(recentDate.getHours() - 1); + + const message: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit() + .create() as JetstreamEventCommit; + + expect(await validator.handle(handlerAgent, message)).toBe(false); + }); + + test('handle returns true if message is created within the last 24 hours', async () => { + const recentDate = new Date(); + recentDate.setHours(recentDate.getHours() - 1); + + const message: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .record({ + createdAt: recentDate.toISOString(), + } as NewSkeetRecord) + .create() + ) + .create() as JetstreamEventCommit; + + expect(await validator.handle(handlerAgent, message)).toBe(true); + }); + + test('handle returns false if message is created more than 24 hours ago', async () => { + const oldDate = new Date(); + oldDate.setDate(oldDate.getDate() - 2); + const message: JetstreamEventCommit = JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .record({ + createdAt: oldDate.toISOString(), + } as NewSkeetRecord) + .create() + ) + .create() as JetstreamEventCommit; + + expect(await validator.handle(handlerAgent, message)).toBe(false); + }); +}); diff --git a/tests/validations/message-validators/post/PostValidators/IsReplyValidator.test.ts b/tests/validations/message-validators/post/PostValidators/IsReplyValidator.test.ts new file mode 100644 index 0000000..78b5011 --- /dev/null +++ b/tests/validations/message-validators/post/PostValidators/IsReplyValidator.test.ts @@ -0,0 +1,72 @@ +import { + HandlerAgent, + IsReplyValidator, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, + ReplyFactory, +} from '../../../../../src'; +import { BskyAgent } from '@atproto/api'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/post/reply'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('IsReplyValidator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + const validator = IsReplyValidator.make(); + const botDid = 'did:plc:bot'; + const bskyAgent: BskyAgent = { + session: { + did: botDid, + }, + } as BskyAgent; + const handlerAgent: HandlerAgent = new HandlerAgent( + 'name', + 'handle', + 'password', + bskyAgent + ); + + const createMessage = (reply: boolean) => { + const recordFactory = + NewSkeetRecordFactory.factory().text('Check reply'); + + if (reply) { + recordFactory.reply( + ReplyFactory.factory().replyTo(botDid).create() + ); + } + + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record(recordFactory.create()) + .create() + ) + .create() as JetstreamEventCommit; + }; + + test('shouldTrigger returns true if op.payload.reply is not null', async () => { + const message = createMessage(true); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe(true); + }); + + test('shouldTrigger returns false if op.payload.reply is null', async () => { + const message = createMessage(false); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe( + false + ); + }); +}); diff --git a/tests/validations/message-validators/post/PostValidators/PostedByUserValidator.test.ts b/tests/validations/message-validators/post/PostValidators/PostedByUserValidator.test.ts new file mode 100644 index 0000000..84ae681 --- /dev/null +++ b/tests/validations/message-validators/post/PostValidators/PostedByUserValidator.test.ts @@ -0,0 +1,63 @@ +import { + HandlerAgent, + JetstreamCollectionType, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, + PostedByUserValidator, +} from '../../../../../src'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/post/postedBy'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('Posted by user validator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + const userDid = 'did:plc:user'; + const validator = PostedByUserValidator.make(userDid); + const handlerAgent: HandlerAgent = {} as HandlerAgent; + + const createMessage = ( + did: string, + collection: JetstreamCollectionType = 'app.bsky.feed.post' + ) => { + return JetstreamEventFactory.factory() + .fromDid(did) + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection(collection) + .record(NewSkeetRecordFactory.factory().create()) + .create() + ) + .create() as JetstreamEventCommit; + }; + + it('shouldTrigger returns true if posted by same did', async () => { + const message = createMessage(userDid); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe(true); + }); + + it('shouldTrigger returns false not posted by same user', async () => { + const message = createMessage('did:plc:other'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe( + false + ); + }); + + it('shouldTrigger returns false if not a post', async () => { + const message = createMessage(userDid, 'app.bsky.feed.like'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe( + false + ); + }); +}); diff --git a/tests/validations/message-validators/post/PostValidators/ReplyingToBotValidator.test.ts b/tests/validations/message-validators/post/PostValidators/ReplyingToBotValidator.test.ts new file mode 100644 index 0000000..5a53d72 --- /dev/null +++ b/tests/validations/message-validators/post/PostValidators/ReplyingToBotValidator.test.ts @@ -0,0 +1,84 @@ +import { + HandlerAgent, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, + ReplyFactory, + ReplyingToBotValidator, +} from '../../../../../src'; +import { BskyAgent } from '@atproto/api'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/post/replyToBot'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('ReplyingToBotValidator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + const validator = ReplyingToBotValidator.make(); + const botDid = 'did:plc:bot'; + + const createHandlerAgent = (): HandlerAgent => { + const bskyAgent: BskyAgent = { + session: { + did: botDid, + }, + } as BskyAgent; + + return new HandlerAgent('name', 'handle', 'password', bskyAgent); + }; + + const createMessage = (replyDid: string | undefined | null = null) => { + const recordFactory = NewSkeetRecordFactory.factory(); + if (replyDid == 'default') { + recordFactory.reply(); + } else if (replyDid) { + recordFactory.reply( + ReplyFactory.factory().replyTo(replyDid).create() + ); + } + + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record(recordFactory.create()) + .create() + ) + .create() as JetstreamEventCommit; + }; + + it('shouldTrigger returns false if no reply', async () => { + const message = createMessage(); + const handlerAgent = createHandlerAgent(); + + expect(await validator.shouldTrigger(handlerAgent, message)).toBe( + false + ); + }); + + it('shouldTrigger returns true if the did is the same as the agent', async () => { + const message = createMessage(botDid); + const handlerAgent = createHandlerAgent(); + + expect(await validator.shouldTrigger(handlerAgent, message)).toBe(true); + }); + + it('shouldTrigger returns false if the did in the reply.parent.uri is not the same as the agent details', async () => { + const message = createMessage('default'); + const handlerAgent = createHandlerAgent(); + + expect(await validator.shouldTrigger(handlerAgent, message)).toBe( + false + ); + }); +}); diff --git a/tests/validations/message-validators/post/StringValidators/InputContainsValidator.test.ts b/tests/validations/message-validators/post/StringValidators/InputContainsValidator.test.ts new file mode 100644 index 0000000..99433dd --- /dev/null +++ b/tests/validations/message-validators/post/StringValidators/InputContainsValidator.test.ts @@ -0,0 +1,83 @@ +import { + HandlerAgent, + InputContainsValidator, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, +} from '../../../../../src'; + +describe('InputContainsValidator no strict parameter', () => { + const validator = InputContainsValidator.make('test'); + const handlerAgent: HandlerAgent = {} as HandlerAgent; + + const createMessage = (text: string) => { + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record(NewSkeetRecordFactory.factory().text(text).create()) + .create() + ) + .create() as JetstreamEventCommit; + }; + + test('shouldTrigger returns true if input contains with trigger keyword', async () => { + const message = createMessage('test message'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe(true); + }); + + test('shouldTrigger returns true if input contains trigger keyword in other words', async () => { + const message = createMessage('blahblahtestblahblah'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe(true); + }); + + test('shouldTrigger returns false if input does not contain trigger keyword', async () => { + const message = createMessage('message example'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe( + false + ); + }); +}); + +describe('InputContainsValidator true strict parameter', () => { + const validator = InputContainsValidator.make('test', true); + const handlerAgent: HandlerAgent = {} as HandlerAgent; + + const createMessage = (text: string) => { + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record(NewSkeetRecordFactory.factory().text(text).create()) + .create() + ) + .create() as JetstreamEventCommit; + }; + + test('shouldTrigger returns true if input contains with trigger keyword', async () => { + const message = createMessage('test message'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe(true); + }); + + test('shouldTrigger returns true if input contains trigger keyword in other words', async () => { + const message = createMessage('blahblahtestblahblah'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe(true); + }); + + test('shouldTrigger returns false if input does not contain trigger keyword', async () => { + const message = createMessage('message example'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe( + false + ); + }); + + test('shouldTrigger returns false if input does not match case sensitivity', async () => { + const message = createMessage('Test'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe( + false + ); + }); +}); diff --git a/tests/validations/post/StringValidators/InputEqualsValidator.test.ts b/tests/validations/message-validators/post/StringValidators/InputEqualsValidator.test.ts similarity index 50% rename from tests/validations/post/StringValidators/InputEqualsValidator.test.ts rename to tests/validations/message-validators/post/StringValidators/InputEqualsValidator.test.ts index 58506cd..c4ec927 100644 --- a/tests/validations/post/StringValidators/InputEqualsValidator.test.ts +++ b/tests/validations/message-validators/post/StringValidators/InputEqualsValidator.test.ts @@ -1,24 +1,36 @@ import { - CreateSkeetMessage, - CreateSkeetMessageFactory, HandlerAgent, InputEqualsValidator, -} from '../../../../src'; + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, +} from '../../../../../src'; describe('InputEqualsValidator', () => { const validator = InputEqualsValidator.make('test'); const handlerAgent: HandlerAgent = {} as HandlerAgent; + const createMessage = (text: string) => { + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record(NewSkeetRecordFactory.factory().text(text).create()) + .create() + ) + .create() as JetstreamEventCommit; + }; + /** * Test: shouldTrigger returns true if input is trigger keyword * This test confirms that the validator correctly returns true when the input * matches the trigger keyword. */ test('shouldTrigger returns true if input is trigger keyword', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('test') - .create(); - expect(await validator.shouldTrigger(message, handlerAgent)).toBe(true); + const message = createMessage('test'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe(true); }); /** @@ -27,10 +39,8 @@ describe('InputEqualsValidator', () => { * does not match the trigger keyword. */ test('shouldTrigger returns false if input does not equal trigger keyword', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('message test') - .create(); - expect(await validator.shouldTrigger(message, handlerAgent)).toBe( + const message = createMessage('message test'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe( false ); }); diff --git a/tests/validations/message-validators/post/StringValidators/InputIsCommandValidator.test.ts b/tests/validations/message-validators/post/StringValidators/InputIsCommandValidator.test.ts new file mode 100644 index 0000000..f00f3e1 --- /dev/null +++ b/tests/validations/message-validators/post/StringValidators/InputIsCommandValidator.test.ts @@ -0,0 +1,146 @@ +import { + HandlerAgent, + InputIsCommandValidator, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecord, + NewSkeetRecordFactory, +} from '../../../../../src'; + +describe('InputIsCommandValidator Class', () => { + let inputIsCommandValidator: InputIsCommandValidator; + const handlerAgent: HandlerAgent = {} as HandlerAgent; + + const createMessage = (text: string) => { + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record(NewSkeetRecordFactory.factory().text(text).create()) + .create() + ) + .create() as JetstreamEventCommit; + }; + + beforeEach(() => { + inputIsCommandValidator = InputIsCommandValidator.make('key'); + }); + + it('should test shouldTrigger function - Prefix case', async () => { + let message = createMessage('!key someCommand'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(true); + + message = createMessage('!key'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(true); + + message = createMessage('someCommand !key'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(false); + + message = createMessage('someCommand'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(false); + }); + + it('should test shouldTrigger function - Suffix case', async () => { + let message = createMessage('key! someCommand'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(true); + + message = createMessage('key!'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(true); + + message = createMessage('someCommand key!'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(false); + + message = createMessage('someCommand'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(false); + }); +}); + +describe('InputIsCommandValidator Not strict Class', () => { + let inputIsCommandValidator: InputIsCommandValidator; + const handlerAgent: HandlerAgent = {} as HandlerAgent; + + const createMessage = (text: string | undefined = undefined) => { + let record: NewSkeetRecord; + if (text == undefined) { + record = NewSkeetRecordFactory.make(); + } else { + record = NewSkeetRecordFactory.factory().text(text).create(); + } + + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record(record) + .create() + ) + .create() as JetstreamEventCommit; + }; + + beforeEach(() => { + inputIsCommandValidator = InputIsCommandValidator.make('key', false); + }); + + it('should test shouldTrigger function - Prefix case', async () => { + let message = createMessage('!Key someCommand'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(true); + + message = createMessage('!keY'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(true); + + message = createMessage('someCommand !key'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(false); + + message = createMessage(); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(false); + }); + + it('should test shouldTrigger function - Suffix case', async () => { + let message = createMessage('keY! someCommand'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(true); + + message = createMessage('Key!'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(true); + + message = createMessage('someCommand key!'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(false); + + message = createMessage('someCommand'); + expect( + await inputIsCommandValidator.shouldTrigger(handlerAgent, message) + ).toBe(false); + }); +}); diff --git a/tests/validations/message-validators/post/StringValidators/InputStartsWithValidator.test.ts b/tests/validations/message-validators/post/StringValidators/InputStartsWithValidator.test.ts new file mode 100644 index 0000000..a9a6ecb --- /dev/null +++ b/tests/validations/message-validators/post/StringValidators/InputStartsWithValidator.test.ts @@ -0,0 +1,45 @@ +import { + HandlerAgent, + InputStartsWithValidator, + JetstreamCommitFactory, + JetstreamEventCommit, + JetstreamEventFactory, + NewSkeetRecordFactory, +} from '../../../../../src'; + +describe('InputStartsWithValidator', () => { + const validator = InputStartsWithValidator.make('test'); + const strictValidator = InputStartsWithValidator.make('test', true); + const handlerAgent: HandlerAgent = {} as HandlerAgent; + + const createMessage = (text: string) => { + return JetstreamEventFactory.factory() + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.post') + .record(NewSkeetRecordFactory.factory().text(text).create()) + .create() + ) + .create() as JetstreamEventCommit; + }; + + test('shouldTrigger returns true if input starts with trigger keyword', async () => { + const message = createMessage('test message'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe(true); + }); + + test('shouldTrigger returns false if input does not start with trigger keyword', async () => { + const message = createMessage('message test'); + expect(await validator.shouldTrigger(handlerAgent, message)).toBe( + false + ); + }); + + test('shouldTrigger in strict mode returns true only if input strictly starts with trigger keyword', async () => { + const message = createMessage('Test message'); + expect(await strictValidator.shouldTrigger(handlerAgent, message)).toBe( + false + ); + }); +}); diff --git a/tests/validations/message-validators/repost/RepostByUser.test.ts b/tests/validations/message-validators/repost/RepostByUser.test.ts new file mode 100644 index 0000000..d115ca4 --- /dev/null +++ b/tests/validations/message-validators/repost/RepostByUser.test.ts @@ -0,0 +1,94 @@ +import { + HandlerAgent, + JetstreamEventCommit, + JetstreamCommitFactory, + JetstreamEventFactory, + JetstreamRecordFactory, + RepostByUser, +} from '../../../../src'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/repost/RepostByUser'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('Repost By User Validator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + + const mockHandlerAgent: HandlerAgent = { + getDid: 'did:plc:bot', + getDIDFromUri: jest.fn().mockReturnValue('did:plc:bot'), + } as unknown as HandlerAgent; + + const createMessage = (subjectUri: string, did: string) => { + return JetstreamEventFactory.factory() + .fromDid(did) + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.repost') + .record( + JetstreamRecordFactory.factory() + .subject({ uri: subjectUri, cid: 'test' }) + .create() + ) + .create() + ) + .create() as JetstreamEventCommit; + }; + + it('handle returns true if userDid is undefined and message did matches handlerAgent did', async () => { + const validator = RepostByUser.make(undefined, 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(true); + }); + + it('handle returns true if provided userDid matches message did', async () => { + const validator = RepostByUser.make('did:plc:bot', 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(true); + }); + + it('handle returns false if message commit record subject is a string', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = 'string' as unknown as string; + + const validator = RepostByUser.make(); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if message commit record subject is undefined', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = undefined; + + const validator = RepostByUser.make(); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if postUri does not match', async () => { + const validator = RepostByUser.make('did:plc:bot', 'different:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if userDid is provided and does not match message did', async () => { + const validator = RepostByUser.make('did:plc:other', 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); +}); diff --git a/tests/validations/message-validators/repost/RepostOfPost.test.ts b/tests/validations/message-validators/repost/RepostOfPost.test.ts new file mode 100644 index 0000000..3037237 --- /dev/null +++ b/tests/validations/message-validators/repost/RepostOfPost.test.ts @@ -0,0 +1,80 @@ +import { + JetstreamEventCommit, + JetstreamCommitFactory, + JetstreamEventFactory, + JetstreamRecordFactory, + RepostOfPost, + HandlerAgent, +} from '../../../../src'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/repost/RepostOfPost'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('Repost Of Post Validator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + + const mockHandlerAgent: HandlerAgent = { + getDid: 'did:plc:bot', + getDIDFromUri: jest.fn().mockReturnValue('did:plc:bot'), + } as unknown as HandlerAgent; + + const createMessage = (subjectUri: string, did: string) => { + return JetstreamEventFactory.factory() + .fromDid(did) + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.repost') + .record( + JetstreamRecordFactory.factory() + .subject({ uri: subjectUri, cid: 'test' }) + .create() + ) + .create() + ) + .create() as JetstreamEventCommit; + }; + + it('handle returns true if postUri matches the message subject uri', async () => { + const validator = RepostOfPost.make('example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(true); + }); + + it('handle returns false if message commit record subject is a string', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = '' as unknown as { uri: string }; + + const validator = RepostOfPost.make('example:uri'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if message commit record subject is undefined', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = undefined; + + const validator = RepostOfPost.make('example:uri'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if postUri does not match the message subject uri', async () => { + const validator = RepostOfPost.make('different:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); +}); diff --git a/tests/validations/message-validators/repost/RepostOfUser.test.ts b/tests/validations/message-validators/repost/RepostOfUser.test.ts new file mode 100644 index 0000000..3e45181 --- /dev/null +++ b/tests/validations/message-validators/repost/RepostOfUser.test.ts @@ -0,0 +1,94 @@ +import { + HandlerAgent, + JetstreamEventCommit, + JetstreamCommitFactory, + JetstreamEventFactory, + JetstreamRecordFactory, + RepostOfUser, +} from '../../../../src'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +const sessPath = './tests/temp/val/repost/RepostOfUser'; +dotenv.config(); +process.env.SESSION_DATA_PATH = sessPath; + +describe('Repost Of User Validator', () => { + afterAll(() => { + fs.rmSync(sessPath, { + recursive: true, + force: true, + }); + }); + fs.mkdirSync(sessPath, { recursive: true }); + + const mockHandlerAgent: HandlerAgent = { + getDid: 'did:plc:bot', + getDIDFromUri: jest.fn().mockReturnValue('did:plc:bot'), + } as unknown as HandlerAgent; + + const createMessage = (subjectUri: string, did: string) => { + return JetstreamEventFactory.factory() + .fromDid(did) + .commit( + JetstreamCommitFactory.factory() + .operation('create') + .collection('app.bsky.feed.repost') + .record( + JetstreamRecordFactory.factory() + .subject({ uri: subjectUri, cid: 'test' }) + .create() + ) + .create() + ) + .create() as JetstreamEventCommit; + }; + + it('handle returns true if userDid is undefined and repost is by same user', async () => { + const validator = RepostOfUser.make(undefined, 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(true); + }); + + it('handle returns true if provided userDid matches reposted post URI DID', async () => { + const validator = RepostOfUser.make('did:plc:bot', 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(true); + }); + + it('handle returns false if message commit record subject is a string', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = 'string'; + + const validator = RepostOfUser.make(); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if message commit record subject is a undefined', async () => { + const message = createMessage('', ''); + // @ts-ignore + message.commit.record.subject = undefined; + + const validator = RepostOfUser.make(); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if postUri does not match', async () => { + const validator = RepostOfUser.make('did:plc:bot', 'different:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); + + it('handle returns false if userDid is provided and does not match reposted post URI DID', async () => { + const validator = RepostOfUser.make('did:plc:other', 'example:uri'); + const message = createMessage('example:uri', 'did:plc:bot'); + + expect(await validator.handle(mockHandlerAgent, message)).toBe(false); + }); +}); diff --git a/tests/validations/post/PostValidators/IsReplyValidator.test.ts b/tests/validations/post/PostValidators/IsReplyValidator.test.ts deleted file mode 100644 index 0ea8273..0000000 --- a/tests/validations/post/PostValidators/IsReplyValidator.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - CreateSkeetMessage, - CreateSkeetMessageFactory, - HandlerAgent, - IsReplyValidator, -} from '../../../../src'; -import { BskyAgent } from '@atproto/api'; - -describe('IsReplyValidator', () => { - const validator = IsReplyValidator.make(); - const botDid = 'did:plc:bot'; - const bskyAgent: BskyAgent = { - session: { - did: botDid, - }, - } as BskyAgent; - const handlerAgent: HandlerAgent = new HandlerAgent( - 'name', - 'handle', - 'password', - bskyAgent - ); - - test('shouldTrigger returns true if op.payload.reply is not null', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withReply() - .create(); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe(true); - }); - - test('shouldTrigger returns false if op.payload.reply is null', async () => { - const message: CreateSkeetMessage = - CreateSkeetMessageFactory.factory().create(); - expect(await validator.shouldTrigger(message, handlerAgent)).toBe( - false - ); - }); -}); diff --git a/tests/validations/post/PostValidators/PostedByUserValidator.test.ts b/tests/validations/post/PostValidators/PostedByUserValidator.test.ts deleted file mode 100644 index d935079..0000000 --- a/tests/validations/post/PostValidators/PostedByUserValidator.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - CreateSkeetMessage, - CreateSkeetMessageFactory, - HandlerAgent, - PostedByUserValidator, -} from '../../../../src'; - -describe('Posted by user validator', () => { - const userDid = 'did:plc:user'; - const validator = PostedByUserValidator.make(userDid); - const handlerAgent: HandlerAgent = {} as HandlerAgent; - - it('shouldTrigger returns true if posted by same did', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .fromDid(userDid) - .create(); - expect(await validator.shouldTrigger(message, handlerAgent)).toBe(true); - }); - - it('shouldTrigger returns false not posted by same user', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .fromDid('did:plc:other') - .create(); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe( - false - ); - }); - - it('shouldTrigger returns false if not a post', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .collection('app.bsky.feed.like') - .create(); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe( - false - ); - }); -}); diff --git a/tests/validations/post/PostValidators/ReplyingToBotValidator.test.ts b/tests/validations/post/PostValidators/ReplyingToBotValidator.test.ts deleted file mode 100644 index f1196b2..0000000 --- a/tests/validations/post/PostValidators/ReplyingToBotValidator.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - CreateSkeetMessage, - CreateSkeetMessageFactory, - HandlerAgent, - ReplyFactory, - ReplyingToBotValidator, -} from '../../../../src'; -import { BskyAgent } from '@atproto/api'; - -describe('ReplyingToBotValidator', () => { - const validator = ReplyingToBotValidator.make(); - const botDid = 'did:plc:bot'; - - it('shouldTrigger returns false if no reply', async () => { - const message: CreateSkeetMessage = - CreateSkeetMessageFactory.factory().create(); - - const bskyAgent: BskyAgent = { - session: { - did: botDid, - }, - } as BskyAgent; - const handlerAgent: HandlerAgent = new HandlerAgent( - 'name', - 'handle', - 'password', - bskyAgent - ); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe( - false - ); - }); - - it('shouldTrigger returns true if the did is the same as the agent', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withReply(ReplyFactory.factory().replyTo(botDid).create()) - .create(); - - const bskyAgent: BskyAgent = { - session: { - did: botDid, - }, - } as BskyAgent; - const handlerAgent: HandlerAgent = new HandlerAgent( - 'name', - 'handle', - 'password', - bskyAgent - ); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe(true); - }); - - it('shouldTrigger returns false if the did in the reply.parent.uri is not the same as the agent details', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withReply(ReplyFactory.factory().replyTo('did:plc:bad').create()) - .create(); - const bskyAgent: BskyAgent = { - session: { - did: botDid, - }, - } as BskyAgent; - const handlerAgent: HandlerAgent = new HandlerAgent( - 'name', - 'handle', - 'password', - bskyAgent - ); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe( - false - ); - }); -}); diff --git a/tests/validations/post/StringValidators/InputContainsValidator.test.ts b/tests/validations/post/StringValidators/InputContainsValidator.test.ts deleted file mode 100644 index 12206dc..0000000 --- a/tests/validations/post/StringValidators/InputContainsValidator.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - CreateSkeetMessage, - CreateSkeetMessageFactory, - HandlerAgent, - InputContainsValidator, -} from '../../../../src'; - -describe('InputContainsValidator no strict parameter', () => { - const validator = InputContainsValidator.make('test'); - const handlerAgent: HandlerAgent = {} as HandlerAgent; - - test('shouldTrigger returns true if input contains with trigger keyword', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('test message') - .create(); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe(true); - }); - - test('shouldTrigger returns true if input contains trigger keyword in other words', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('blahblahtestblahblah') - .create(); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe(true); - }); - - test('shouldTrigger returns false if input does not contain trigger keyword', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('message example') - .create(); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe( - false - ); - }); -}); - -describe('InputContainsValidator true strict parameter', () => { - const validator = InputContainsValidator.make('test', true); - const handlerAgent: HandlerAgent = {} as HandlerAgent; - - test('shouldTrigger returns true if input contains with trigger keyword', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('test message') - .create(); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe(true); - }); - - test('shouldTrigger returns true if input contains trigger keyword in other words', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('blahblahtestblahblah') - .create(); - expect(await validator.shouldTrigger(message, handlerAgent)).toBe(true); - }); - - test('shouldTrigger returns false if input does not contain trigger keyword', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('message example') - .create(); - - expect(await validator.shouldTrigger(message, handlerAgent)).toBe( - false - ); - }); - - test('shouldTrigger returns false if input does not match case sensitivity', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('Test') - .create(); - expect(await validator.shouldTrigger(message, handlerAgent)).toBe( - false - ); - }); -}); diff --git a/tests/validations/post/StringValidators/InputIsCommandValidator.test.ts b/tests/validations/post/StringValidators/InputIsCommandValidator.test.ts deleted file mode 100644 index 0c21f76..0000000 --- a/tests/validations/post/StringValidators/InputIsCommandValidator.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - CreateSkeetMessage, - CreateSkeetMessageFactory, - HandlerAgent, - InputIsCommandValidator, -} from '../../../../src'; - -describe('InputIsCommandValidator Class', () => { - let inputIsCommandValidator: InputIsCommandValidator; - let message: CreateSkeetMessage; - const handlerAgent: HandlerAgent = {} as HandlerAgent; - - beforeEach(() => { - inputIsCommandValidator = InputIsCommandValidator.make('key'); - message = CreateSkeetMessageFactory.factory() - .withText('key! someCommand') - .create(); - }); - - it('should test shouldTrigger function - Prefix case', async () => { - message.record.text = '!key someCommand'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(true); - - message.record.text = '!key'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(true); - - message.record.text = 'someCommand !key'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(false); - - message.record.text = 'someCommand'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(false); - }); - - it('should test shouldTrigger function - Suffix case', async () => { - message.record.text = 'key! someCommand'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(true); - - message.record.text = 'key!'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(true); - - message.record.text = 'someCommand key!'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(false); - - message.record.text = 'someCommand'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(false); - }); -}); - -describe('InputIsCommandValidator Not strict Class', () => { - let inputIsCommandValidator: InputIsCommandValidator; - let message: CreateSkeetMessage; - const handlerAgent: HandlerAgent = {} as HandlerAgent; - - beforeEach(() => { - inputIsCommandValidator = InputIsCommandValidator.make('key', false); - message = CreateSkeetMessageFactory.factory().withText('test').create(); - }); - - it('should test shouldTrigger function - Prefix case', async () => { - message.record.text = '!Key someCommand'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(true); - - message.record.text = '!keY'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(true); - - message.record.text = 'someCommand !key'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(false); - - message.record.text = 'someCommand'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(false); - }); - - it('should test shouldTrigger function - Suffix case', async () => { - message.record.text = 'keY! someCommand'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(true); - - message.record.text = 'Key!'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(true); - - message.record.text = 'someCommand key!'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(false); - - message.record.text = 'someCommand'; - expect( - await inputIsCommandValidator.shouldTrigger(message, handlerAgent) - ).toBe(false); - }); -}); diff --git a/tests/validations/post/StringValidators/InputStartsWithValidator.test.ts b/tests/validations/post/StringValidators/InputStartsWithValidator.test.ts deleted file mode 100644 index c5ade2d..0000000 --- a/tests/validations/post/StringValidators/InputStartsWithValidator.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - CreateSkeetMessage, - CreateSkeetMessageFactory, - HandlerAgent, - InputStartsWithValidator, -} from '../../../../src'; - -describe('InputStartsWithValidator', () => { - const validator = InputStartsWithValidator.make('test'); - const strictValidator = InputStartsWithValidator.make('test', true); - const handlerAgent: HandlerAgent = {} as HandlerAgent; - - test('shouldTrigger returns true if input starts with trigger keyword', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('test message') - .create(); - expect(await validator.shouldTrigger(message, handlerAgent)).toBe(true); - }); - - test('shouldTrigger returns false if input does not start with trigger keyword', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('message test') - .create(); - expect(await validator.shouldTrigger(message, handlerAgent)).toBe( - false - ); - }); - - test('shouldTrigger in strict mode returns true only if input strictly starts with trigger keyword', async () => { - const message: CreateSkeetMessage = CreateSkeetMessageFactory.factory() - .withText('Test message') - .create(); - expect(await strictValidator.shouldTrigger(message, handlerAgent)).toBe( - false - ); - }); -});