├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature-requests.md ├── README.md └── workflows │ ├── lint.yml │ ├── manual-release.yml │ ├── pr-comment.yml │ ├── publish-release.yml │ ├── stale.yml │ ├── update-nightly.yml │ └── update-proto.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .release-it.yml ├── CHANGELOG.md ├── Example ├── boot_analytics_test.json └── example.ts ├── LICENSE ├── Media ├── .gitignore ├── cat.jpeg ├── icon.png ├── logo.png ├── ma_gif.mp4 ├── meme.jpeg ├── octopus.webp └── sonata.mp3 ├── README.md ├── WAProto ├── GenerateStatics.sh ├── WAProto.proto ├── index.d.ts └── index.js ├── WASignalGroup ├── GroupProtocol.js ├── ciphertext_message.js ├── generate-proto.sh ├── group.proto ├── group_cipher.js ├── group_session_builder.js ├── index.js ├── keyhelper.js ├── protobufs.js ├── queue_job.js ├── readme.md ├── sender_chain_key.js ├── sender_key_distribution_message.js ├── sender_key_message.js ├── sender_key_name.js ├── sender_key_record.js ├── sender_key_state.js └── sender_message_key.js ├── engine-requirements.js ├── jest.config.js ├── package.json ├── proto-extract ├── README.md ├── index.js ├── package-lock.json ├── package.json └── yarn.lock ├── src ├── Defaults │ ├── baileys-version.json │ └── index.ts ├── Signal │ └── libsignal.ts ├── Socket │ ├── Client │ │ ├── index.ts │ │ ├── types.ts │ │ └── websocket.ts │ ├── business.ts │ ├── chats.ts │ ├── groups.ts │ ├── index.ts │ ├── messages-recv.ts │ ├── messages-send.ts │ ├── socket.ts │ └── usync.ts ├── Tests │ ├── test.app-state-sync.ts │ ├── test.event-buffer.ts │ ├── test.key-store.ts │ ├── test.libsignal.ts │ ├── test.media-download.ts │ ├── test.messages.ts │ └── utils.ts ├── Types │ ├── Auth.ts │ ├── Call.ts │ ├── Chat.ts │ ├── Contact.ts │ ├── Events.ts │ ├── GroupMetadata.ts │ ├── Label.ts │ ├── LabelAssociation.ts │ ├── Message.ts │ ├── Product.ts │ ├── Signal.ts │ ├── Socket.ts │ ├── State.ts │ ├── USync.ts │ └── index.ts ├── Utils │ ├── auth-utils.ts │ ├── baileys-event-stream.ts │ ├── business.ts │ ├── chat-utils.ts │ ├── crypto.ts │ ├── decode-wa-message.ts │ ├── event-buffer.ts │ ├── generics.ts │ ├── history.ts │ ├── index.ts │ ├── link-preview.ts │ ├── logger.ts │ ├── lt-hash.ts │ ├── make-mutex.ts │ ├── messages-media.ts │ ├── messages.ts │ ├── noise-handler.ts │ ├── process-message.ts │ ├── signal.ts │ ├── use-multi-file-auth-state.ts │ └── validate-connection.ts ├── WABinary │ ├── constants.ts │ ├── decode.ts │ ├── encode.ts │ ├── generic-utils.ts │ ├── index.ts │ ├── jid-utils.ts │ └── types.ts ├── WAM │ ├── BinaryInfo.ts │ ├── constants.ts │ ├── encode.ts │ └── index.ts ├── WAUSync │ ├── Protocols │ │ ├── USyncContactProtocol.ts │ │ ├── USyncDeviceProtocol.ts │ │ ├── USyncDisappearingModeProtocol.ts │ │ ├── USyncStatusProtocol.ts │ │ ├── UsyncBotProfileProtocol.ts │ │ ├── UsyncLIDProtocol.ts │ │ └── index.ts │ ├── USyncQuery.ts │ ├── USyncUser.ts │ └── index.ts └── index.ts ├── tsconfig.json ├── typedoc.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | lib 3 | coverage 4 | *.lock 5 | .eslintrc.json 6 | src/WABinary/index.ts 7 | WAProto 8 | WASignalGroup 9 | Example/Example.ts 10 | docs 11 | proto-extract -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@whiskeysockets", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "project": "./tsconfig.json" 6 | }, 7 | "ignorePatterns": ["src/Tests/*"], 8 | "rules": { 9 | "@typescript-eslint/no-explicit-any": [ 10 | "warn", 11 | { 12 | "ignoreRestArgs": true 13 | } 14 | ], 15 | "@typescript-eslint/no-inferrable-types": [ 16 | "warn" 17 | ], 18 | "@typescript-eslint/no-redundant-type-constituents": [ 19 | "warn" 20 | ], 21 | "@typescript-eslint/no-unnecessary-type-assertion": [ 22 | "warn" 23 | ], 24 | "no-restricted-syntax": "off", 25 | "keyword-spacing": [ 26 | "warn" 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [purpshell, auties00, SheIITear] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve the library 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Created a new connection 16 | 2. Closed & used saved credentials to log back in 17 | 3. Etc. 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Environment (please complete the following information):** 23 | - Is this on a server? 24 | - What do your `connectOptions` look like? 25 | - Do you have multiple clients on the same IP? 26 | - Are you using a proxy? 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Have a question? 3 | about: Join our discord and send a post in the `#baileys-help` channel 4 | url: https://discord.gg/WeJM5FP9GG 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-requests.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Requests 3 | about: Template for general issues/feature requests 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Before adding this issue, make sure you do the following to make sure this is not a duplicate:** 11 | 1. Search through the repo's previous issues 12 | 2. Read through the readme at least once 13 | 3. Search the docs for the feature you're looking for 14 | 15 | **Just describe the feature** 16 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 |

Baileys logo

2 | 3 | ![NPM Downloads](https://img.shields.io/npm/dw/%40whiskeysockets%2Fbaileys?label=npm&color=%23CB3837) 4 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/whiskeysockets/baileys) 5 | ![Discord](https://img.shields.io/discord/725839806084546610?label=discord&color=%235865F2) 6 | 7 | Baileys is a WebSockets-based TypeScript library for interacting with the WhatsApp Web API. 8 | 9 | # Usage 10 | A new guide has been posted at https://baileys.wiki. The old guide can be accessed on [NPM](https://npmjs.com/package/baileys). 11 | 12 | # Sponsor 13 | If you'd like to financially support this project, you can do so by supporting the current maintainer [here](https://purpshell.dev/sponsor). 14 | 15 | # Disclaimer 16 | This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or its affiliates. 17 | The official WhatsApp website can be found at whatsapp.com. "WhatsApp" as well as related names, marks, emblems and images are registered trademarks of their respective owners. 18 | 19 | The maintainers of Baileys do not in any way condone the use of this application in practices that violate the Terms of Service of WhatsApp. The maintainers of this application call upon the personal responsibility of its users to use this application in a fair way, as it is intended to be used. 20 | Use at your own discretion. Do not spam people with this. We discourage any stalkerware, bulk or automated messaging usage. 21 | 22 | # License 23 | Copyright (c) 2025 Rajeh Taher/WhiskeySockets 24 | 25 | Licensed under the MIT License: 26 | Permission is hereby granted, free of charge, to any person obtaining a copy 27 | of this software and associated documentation files (the "Software"), to deal 28 | in the Software without restriction, including without limitation the rights 29 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 30 | copies of the Software, and to permit persons to whom the Software is 31 | furnished to do so, subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in all 34 | copies or substantial portions of the Software. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 37 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 38 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 39 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 40 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 41 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 42 | SOFTWARE. 43 | 44 | Thus, the maintainers of the project can't be held liable for any potential misuse of this project. 45 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Check PR health 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | check-lint: 7 | runs-on: ubuntu-latest 8 | timeout-minutes: 10 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Install Node 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 20.x 17 | 18 | - name: Install packages 19 | run: yarn 20 | 21 | - name: Check linting 22 | run: yarn lint 23 | -------------------------------------------------------------------------------- /.github/workflows/manual-release.yml: -------------------------------------------------------------------------------- 1 | name: Manual Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | increment: 7 | type: string 8 | description: "Must be: patch, minor, major, pre* or " 9 | required: true 10 | default: "patch" 11 | jobs: 12 | manual-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | token: ${{ secrets.PERSONAL_TOKEN }} 19 | 20 | - name: Setup GIT 21 | run: | 22 | git config --global user.name "github-actions[bot]" 23 | git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" 24 | 25 | - name: Setup Node 26 | uses: actions/setup-node@v3.6.0 27 | with: 28 | node-version: 20.x 29 | 30 | - name: Get yarn cache directory path 31 | id: yarn-cache-dir-path 32 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT 33 | 34 | - uses: actions/cache@v3 35 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 36 | with: 37 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 38 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-yarn- 41 | 42 | - name: Install Dependencies 43 | run: yarn 44 | 45 | - name: Release 46 | run: "npx release-it --increment ${{ github.event.inputs.increment }}" 47 | -------------------------------------------------------------------------------- /.github/workflows/pr-comment.yml: -------------------------------------------------------------------------------- 1 | name: PR Comment 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | - unlocked 10 | 11 | jobs: 12 | pr-comment: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 10 15 | permissions: write-all 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: mshick/add-pr-comment@v2 20 | with: 21 | repo-token: ${{ secrets.PERSONAL_TOKEN}} 22 | message-id: pr-test 23 | message: | 24 | Thanks for your contribution. 25 | 26 | The next step is to wait for review and approval to merge it to main repository 27 | 28 | The community can help reacting with a thumb up (:thumbsup:) for approval and rocket (:rocket:) for who has tested it. 29 | 30 | To test this PR you can run the following command below: 31 | ``` 32 | # NPM 33 | npm install @whiskeysockets/baileys@${{ github.event.pull_request.head.repo.full_name }}#${{ github.event.pull_request.head.ref }} 34 | # YARN v2 35 | yarn add @whiskeysockets/baileys@${{ github.event.pull_request.head.repo.full_name }}#${{ github.event.pull_request.head.ref }} 36 | ``` 37 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: write-all 9 | 10 | jobs: 11 | publish-release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Fetching tags 20 | run: git fetch --tags -f || true 21 | 22 | - name: Setup Node 23 | uses: actions/setup-node@v3.6.0 24 | with: 25 | node-version: 20.x 26 | registry-url: "https://registry.npmjs.org" 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | 30 | - name: Get yarn cache directory path 31 | id: yarn-cache-dir-path 32 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT 33 | 34 | - uses: actions/cache@v3 35 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 36 | with: 37 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 38 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-yarn- 41 | 42 | - name: Install Dependencies 43 | run: yarn 44 | env: 45 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | 47 | - name: Publish in NPM (as `baileys`) 48 | run: npm publish --access public 49 | env: 50 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | 52 | - name: Publish in NPM (whiskeysockets scope) 53 | run: | 54 | npx json -I -f package.json -e "this.name='@whiskeysockets/baileys'" 55 | npm publish --access public --//registry.npmjs.org/:_authToken=$NPM_TOKEN 56 | npx json -I -f package.json -e "this.name='baileys'" 57 | env: 58 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 59 | 60 | - name: Generate Changelog 61 | id: generate_changelog 62 | run: | 63 | changelog=$(npm run changelog:last --silent) 64 | echo "changelog<> $GITHUB_OUTPUT 65 | echo "${changelog}" >> $GITHUB_OUTPUT 66 | echo "EOF" >> $GITHUB_OUTPUT 67 | 68 | - name: Make Package 69 | run: npm pack 70 | 71 | - name: Rename Pack 72 | run: mv *.tgz baileys.tgz 73 | 74 | - name: Create Release 75 | uses: meeDamian/github-release@2.0 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | with: 79 | token: ${{ secrets.GITHUB_TOKEN }} 80 | tag: ${{ github.ref }} 81 | commitish: ${{ github.sha }} 82 | name: ${{ github.ref_name }} 83 | body: ${{ steps.generate_changelog.outputs.changelog }} 84 | draft: false 85 | prerelease: false 86 | files: > 87 | baileys.tgz 88 | gzip: folders 89 | allow_override: true 90 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | jobs: 10 | stale: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/stale@v3 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue is stale because it has been open 15 days with no activity. Remove the stale label or comment or this will be closed in 15 days' 17 | stale-pr-message: 'This PR is stale because it has been open 15 days with no activity. Remove the stale label or comment or this will be closed in 15 days' 18 | days-before-stale: 15 19 | days-before-close: 30 20 | exempt-issue-labels: 'bug,enhancement' 21 | exempt-pr-labels: 'bug,enhancement' 22 | -------------------------------------------------------------------------------- /.github/workflows/update-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Update Nightly 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | update-nightly: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Fetching tags 21 | run: git fetch --tags -f || true 22 | 23 | - name: Setup Node 24 | uses: actions/setup-node@v3.6.0 25 | with: 26 | node-version: 20.x 27 | 28 | - name: Get yarn cache directory path 29 | id: yarn-cache-dir-path 30 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT 31 | 32 | - uses: actions/cache@v3 33 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 34 | with: 35 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 36 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-yarn- 39 | 40 | - name: Install Dependencies 41 | run: yarn 42 | 43 | - name: Update version to alpha 44 | run: yarn version --prerelease --preid=alpha --no-git --no-git-tag-version 45 | 46 | - name: Build NPM package 47 | run: yarn pack && mv baileys-*.tgz baileys-nightly.tgz 48 | 49 | - name: Generate Changelog 50 | id: generate_changelog 51 | run: | 52 | changelog=$(yarn run --silent changelog:preview) 53 | echo "changelog<> $GITHUB_OUTPUT 54 | echo "${changelog}" >> $GITHUB_OUTPUT 55 | echo "EOF" >> $GITHUB_OUTPUT 56 | 57 | - name: Update Nightly TAG 58 | uses: richardsimko/update-tag@v1 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | with: 62 | tag_name: nightly 63 | 64 | - name: Update Nightly Release 65 | uses: meeDamian/github-release@2.0 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | with: 69 | token: ${{ secrets.GITHUB_TOKEN }} 70 | tag: nightly 71 | commitish: ${{ github.sha }} 72 | name: Nightly Release 73 | body: ${{ steps.generate_changelog.outputs.changelog }} 74 | draft: false 75 | prerelease: true 76 | files: > 77 | baileys-nightly.tgz 78 | gzip: folders 79 | allow_override: true 80 | -------------------------------------------------------------------------------- /.github/workflows/update-proto.yml: -------------------------------------------------------------------------------- 1 | name: Update WAProto 2 | 3 | on: 4 | schedule: 5 | - cron: "10 1 * * *" 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | update-proto: 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 10 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Install Node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 20.x 24 | 25 | - name: Install packages 26 | run: | 27 | yarn 28 | yarn --pure-lockfile --cwd proto-extract 29 | 30 | - name: Update WAProto.proto 31 | id: wa_proto_info 32 | run: | 33 | yarn --cwd proto-extract start > wa-logs.txt 34 | WA_VERSION=$(cat wa-logs.txt | perl -n -e'/Current version\: (.+)/ && print $1') 35 | WA_JS_URL=$(cat wa-logs.txt | perl -n -e'/Found source JS URL\: (.+)/ && print $1') 36 | echo "wa_version=$WA_VERSION" >> $GITHUB_OUTPUT 37 | echo "wa_js_url=$WA_JS_URL" >> $GITHUB_OUTPUT 38 | 39 | - name: GenerateStatics 40 | run: yarn gen:protobuf 41 | 42 | - name: Update baileys-version.json 43 | run: | 44 | WA_VERSION="${{steps.wa_proto_info.outputs.wa_version}}" 45 | WA_NUMBERS=$(echo $WA_VERSION | sed "s/\./, /g") 46 | echo -e "{\n\t\"version\": [$WA_NUMBERS]\n}" > src/Defaults/baileys-version.json 47 | 48 | - name: Create Pull Request 49 | uses: peter-evans/create-pull-request@v5 50 | with: 51 | commit-message: "chore: updated proto/version to v${{steps.wa_proto_info.outputs.wa_version}}" 52 | title: "Whatsapp v${{steps.wa_proto_info.outputs.wa_version}} proto/version change" 53 | branch: "update-proto/stable" 54 | delete-branch: true 55 | labels: "update-proto" 56 | body: "Automated changes\nFound source JS URL: ${{steps.wa_proto_info.outputs.wa_js_url}}\nCurrent version: v${{steps.wa_proto_info.outputs.wa_version}}" 57 | add-paths: | 58 | WAProto/* 59 | src/Defaults/baileys-version.json 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .yarn/ 4 | *.tgz 5 | */.DS_Store 6 | auth_info*.json 7 | baileys_auth_info* 8 | baileys_store*.json 9 | browser-messages.json 10 | browser-token.json 11 | decoded-ws.json 12 | docs 13 | lib 14 | messages*.json 15 | node_modules 16 | output.csv 17 | Proxy 18 | test.ts 19 | TestData 20 | wa-logs.txt -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/Baileys/0c8efaac922b301a0fc8c3a0c4c1196e4346c0d1/.npmignore -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /.release-it.yml: -------------------------------------------------------------------------------- 1 | git: 2 | commitMessage: "chore(release): v${version}" 3 | tagAnnotation: "chore(release): v${version}" 4 | tagName: "v${version}" 5 | 6 | hooks: 7 | after:bump: 8 | - "npm run changelog:update" 9 | 10 | # automatic publish from github workflow 11 | npm: 12 | publish: false 13 | private: true 14 | registry: "OMITTED" 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 6.7.18 (2025-05-28) 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Rajeh Taher/WhiskeySockets 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Media/.gitignore: -------------------------------------------------------------------------------- 1 | received_* 2 | media_* -------------------------------------------------------------------------------- /Media/cat.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/Baileys/0c8efaac922b301a0fc8c3a0c4c1196e4346c0d1/Media/cat.jpeg -------------------------------------------------------------------------------- /Media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/Baileys/0c8efaac922b301a0fc8c3a0c4c1196e4346c0d1/Media/icon.png -------------------------------------------------------------------------------- /Media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/Baileys/0c8efaac922b301a0fc8c3a0c4c1196e4346c0d1/Media/logo.png -------------------------------------------------------------------------------- /Media/ma_gif.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/Baileys/0c8efaac922b301a0fc8c3a0c4c1196e4346c0d1/Media/ma_gif.mp4 -------------------------------------------------------------------------------- /Media/meme.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/Baileys/0c8efaac922b301a0fc8c3a0c4c1196e4346c0d1/Media/meme.jpeg -------------------------------------------------------------------------------- /Media/octopus.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/Baileys/0c8efaac922b301a0fc8c3a0c4c1196e4346c0d1/Media/octopus.webp -------------------------------------------------------------------------------- /Media/sonata.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvolutionAPI/Baileys/0c8efaac922b301a0fc8c3a0c4c1196e4346c0d1/Media/sonata.mp3 -------------------------------------------------------------------------------- /WAProto/GenerateStatics.sh: -------------------------------------------------------------------------------- 1 | yarn pbjs -t static-module -w commonjs -o ./WAProto/index.js ./WAProto/WAProto.proto; 2 | yarn pbts -o ./WAProto/index.d.ts ./WAProto/index.js; -------------------------------------------------------------------------------- /WASignalGroup/ciphertext_message.js: -------------------------------------------------------------------------------- 1 | class CiphertextMessage { 2 | UNSUPPORTED_VERSION = 1; 3 | 4 | CURRENT_VERSION = 3; 5 | 6 | WHISPER_TYPE = 2; 7 | 8 | PREKEY_TYPE = 3; 9 | 10 | SENDERKEY_TYPE = 4; 11 | 12 | SENDERKEY_DISTRIBUTION_TYPE = 5; 13 | 14 | ENCRYPTED_MESSAGE_OVERHEAD = 53; 15 | } 16 | module.exports = CiphertextMessage; -------------------------------------------------------------------------------- /WASignalGroup/generate-proto.sh: -------------------------------------------------------------------------------- 1 | yarn pbjs -t static-module -w commonjs -o ./WASignalGroup/GroupProtocol.js ./WASignalGroup/group.proto -------------------------------------------------------------------------------- /WASignalGroup/group.proto: -------------------------------------------------------------------------------- 1 | package groupproto; 2 | 3 | message SenderKeyMessage { 4 | optional uint32 id = 1; 5 | optional uint32 iteration = 2; 6 | optional bytes ciphertext = 3; 7 | } 8 | 9 | message SenderKeyDistributionMessage { 10 | optional uint32 id = 1; 11 | optional uint32 iteration = 2; 12 | optional bytes chainKey = 3; 13 | optional bytes signingKey = 4; 14 | } 15 | 16 | message SenderChainKey { 17 | optional uint32 iteration = 1; 18 | optional bytes seed = 2; 19 | } 20 | 21 | message SenderMessageKey { 22 | optional uint32 iteration = 1; 23 | optional bytes seed = 2; 24 | } 25 | 26 | message SenderSigningKey { 27 | optional bytes public = 1; 28 | optional bytes private = 2; 29 | } 30 | 31 | message SenderKeyStateStructure { 32 | 33 | 34 | optional uint32 senderKeyId = 1; 35 | optional SenderChainKey senderChainKey = 2; 36 | optional SenderSigningKey senderSigningKey = 3; 37 | repeated SenderMessageKey senderMessageKeys = 4; 38 | } 39 | 40 | message SenderKeyRecordStructure { 41 | repeated SenderKeyStateStructure senderKeyStates = 1; 42 | } -------------------------------------------------------------------------------- /WASignalGroup/group_cipher.js: -------------------------------------------------------------------------------- 1 | const queue_job = require('./queue_job'); 2 | const SenderKeyMessage = require('./sender_key_message'); 3 | const crypto = require('libsignal/src/crypto'); 4 | 5 | class GroupCipher { 6 | constructor(senderKeyStore, senderKeyName) { 7 | this.senderKeyStore = senderKeyStore; 8 | this.senderKeyName = senderKeyName; 9 | } 10 | 11 | queueJob(awaitable) { 12 | return queue_job(this.senderKeyName.toString(), awaitable) 13 | } 14 | 15 | async encrypt(paddedPlaintext) { 16 | return await this.queueJob(async () => { 17 | const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName); 18 | if (!record) { 19 | throw new Error("No SenderKeyRecord found for encryption") 20 | } 21 | const senderKeyState = record.getSenderKeyState(); 22 | if (!senderKeyState) { 23 | throw new Error("No session to encrypt message"); 24 | } 25 | const iteration = senderKeyState.getSenderChainKey().getIteration() 26 | const senderKey = this.getSenderKey(senderKeyState, iteration === 0 ? 0 : iteration + 1) 27 | 28 | const ciphertext = await this.getCipherText( 29 | senderKey.getIv(), 30 | senderKey.getCipherKey(), 31 | paddedPlaintext 32 | ); 33 | 34 | const senderKeyMessage = new SenderKeyMessage( 35 | senderKeyState.getKeyId(), 36 | senderKey.getIteration(), 37 | ciphertext, 38 | senderKeyState.getSigningKeyPrivate() 39 | ); 40 | await this.senderKeyStore.storeSenderKey(this.senderKeyName, record); 41 | return senderKeyMessage.serialize() 42 | }) 43 | } 44 | 45 | async decrypt(senderKeyMessageBytes) { 46 | return await this.queueJob(async () => { 47 | const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName); 48 | if (!record) { 49 | throw new Error("No SenderKeyRecord found for decryption") 50 | } 51 | const senderKeyMessage = new SenderKeyMessage(null, null, null, null, senderKeyMessageBytes); 52 | const senderKeyState = record.getSenderKeyState(senderKeyMessage.getKeyId()); 53 | if (!senderKeyState) { 54 | throw new Error("No session found to decrypt message") 55 | } 56 | 57 | senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic()); 58 | const senderKey = this.getSenderKey(senderKeyState, senderKeyMessage.getIteration()); 59 | // senderKeyState.senderKeyStateStructure.senderSigningKey.private = 60 | 61 | const plaintext = await this.getPlainText( 62 | senderKey.getIv(), 63 | senderKey.getCipherKey(), 64 | senderKeyMessage.getCipherText() 65 | ); 66 | 67 | await this.senderKeyStore.storeSenderKey(this.senderKeyName, record); 68 | 69 | return plaintext; 70 | }) 71 | } 72 | 73 | getSenderKey(senderKeyState, iteration) { 74 | let senderChainKey = senderKeyState.getSenderChainKey(); 75 | if (senderChainKey.getIteration() > iteration) { 76 | if (senderKeyState.hasSenderMessageKey(iteration)) { 77 | return senderKeyState.removeSenderMessageKey(iteration); 78 | } 79 | throw new Error( 80 | `Received message with old counter: ${senderChainKey.getIteration()}, ${iteration}` 81 | ); 82 | } 83 | 84 | if (iteration - senderChainKey.getIteration() > 2000) { 85 | throw new Error('Over 2000 messages into the future!'); 86 | } 87 | 88 | while (senderChainKey.getIteration() < iteration) { 89 | senderKeyState.addSenderMessageKey(senderChainKey.getSenderMessageKey()); 90 | senderChainKey = senderChainKey.getNext(); 91 | } 92 | 93 | senderKeyState.setSenderChainKey(senderChainKey.getNext()); 94 | return senderChainKey.getSenderMessageKey(); 95 | } 96 | 97 | getPlainText(iv, key, ciphertext) { 98 | try { 99 | const plaintext = crypto.decrypt(key, ciphertext, iv); 100 | return plaintext; 101 | } catch (e) { 102 | //console.log(e.stack); 103 | throw new Error('InvalidMessageException'); 104 | } 105 | } 106 | 107 | getCipherText(iv, key, plaintext) { 108 | try { 109 | iv = typeof iv === 'string' ? Buffer.from(iv, 'base64') : iv; 110 | key = typeof key === 'string' ? Buffer.from(key, 'base64') : key; 111 | const crypted = crypto.encrypt(key, Buffer.from(plaintext), iv); 112 | return crypted; 113 | } catch (e) { 114 | //console.log(e.stack); 115 | throw new Error('InvalidMessageException'); 116 | } 117 | } 118 | } 119 | 120 | module.exports = GroupCipher; -------------------------------------------------------------------------------- /WASignalGroup/group_session_builder.js: -------------------------------------------------------------------------------- 1 | //const utils = require('../../common/utils'); 2 | const SenderKeyDistributionMessage = require('./sender_key_distribution_message'); 3 | 4 | const keyhelper = require("./keyhelper"); 5 | class GroupSessionBuilder { 6 | constructor(senderKeyStore) { 7 | this.senderKeyStore = senderKeyStore; 8 | } 9 | 10 | async process(senderKeyName, senderKeyDistributionMessage) { 11 | //console.log('GroupSessionBuilder process', senderKeyName, senderKeyDistributionMessage); 12 | const senderKeyRecord = await this.senderKeyStore.loadSenderKey(senderKeyName); 13 | senderKeyRecord.addSenderKeyState( 14 | senderKeyDistributionMessage.getId(), 15 | senderKeyDistributionMessage.getIteration(), 16 | senderKeyDistributionMessage.getChainKey(), 17 | senderKeyDistributionMessage.getSignatureKey() 18 | ); 19 | await this.senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord); 20 | } 21 | 22 | // [{"senderKeyId":1742199468,"senderChainKey":{"iteration":0,"seed":"yxMY9VFQcXEP34olRAcGCtsgx1XoKsHfDIh+1ea4HAQ="},"senderSigningKey":{"public":""}}] 23 | async create(senderKeyName) { 24 | const senderKeyRecord = await this.senderKeyStore.loadSenderKey(senderKeyName); 25 | //console.log('GroupSessionBuilder create session', senderKeyName, senderKeyRecord); 26 | 27 | if (senderKeyRecord.isEmpty()) { 28 | const keyId = keyhelper.generateSenderKeyId(); 29 | const senderKey = keyhelper.generateSenderKey(); 30 | const signingKey = keyhelper.generateSenderSigningKey(); 31 | 32 | senderKeyRecord.setSenderKeyState(keyId, 0, senderKey, signingKey); 33 | await this.senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord); 34 | } 35 | 36 | const state = senderKeyRecord.getSenderKeyState(); 37 | 38 | return new SenderKeyDistributionMessage( 39 | state.getKeyId(), 40 | state.getSenderChainKey().getIteration(), 41 | state.getSenderChainKey().getSeed(), 42 | state.getSigningKeyPublic() 43 | ); 44 | } 45 | } 46 | module.exports = GroupSessionBuilder; -------------------------------------------------------------------------------- /WASignalGroup/index.js: -------------------------------------------------------------------------------- 1 | module.exports.GroupSessionBuilder = require('./group_session_builder') 2 | module.exports.SenderKeyDistributionMessage = require('./sender_key_distribution_message') 3 | module.exports.SenderKeyRecord = require('./sender_key_record') 4 | module.exports.SenderKeyName = require('./sender_key_name') 5 | module.exports.GroupCipher = require('./group_cipher') -------------------------------------------------------------------------------- /WASignalGroup/keyhelper.js: -------------------------------------------------------------------------------- 1 | const curve = require('libsignal/src/curve'); 2 | const nodeCrypto = require('crypto'); 3 | 4 | exports.generateSenderKey = function() { 5 | return nodeCrypto.randomBytes(32); 6 | } 7 | 8 | exports.generateSenderKeyId = function() { 9 | return nodeCrypto.randomInt(2147483647); 10 | } 11 | 12 | exports.generateSenderSigningKey = function(key) { 13 | if (!key) { 14 | key = curve.generateKeyPair(); 15 | } 16 | 17 | return { 18 | public: key.pubKey, 19 | private: key.privKey, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /WASignalGroup/protobufs.js: -------------------------------------------------------------------------------- 1 | const { groupproto } = require('./GroupProtocol') 2 | 3 | module.exports = groupproto -------------------------------------------------------------------------------- /WASignalGroup/queue_job.js: -------------------------------------------------------------------------------- 1 | // vim: ts=4:sw=4:expandtab 2 | 3 | /* 4 | * jobQueue manages multiple queues indexed by device to serialize 5 | * session io ops on the database. 6 | */ 7 | 'use strict'; 8 | 9 | 10 | const _queueAsyncBuckets = new Map(); 11 | const _gcLimit = 10000; 12 | 13 | async function _asyncQueueExecutor(queue, cleanup) { 14 | let offt = 0; 15 | while (true) { 16 | let limit = Math.min(queue.length, _gcLimit); // Break up thundering hurds for GC duty. 17 | for (let i = offt; i < limit; i++) { 18 | const job = queue[i]; 19 | try { 20 | job.resolve(await job.awaitable()); 21 | } catch (e) { 22 | job.reject(e); 23 | } 24 | } 25 | if (limit < queue.length) { 26 | /* Perform lazy GC of queue for faster iteration. */ 27 | if (limit >= _gcLimit) { 28 | queue.splice(0, limit); 29 | offt = 0; 30 | } else { 31 | offt = limit; 32 | } 33 | } else { 34 | break; 35 | } 36 | } 37 | cleanup(); 38 | } 39 | 40 | module.exports = function (bucket, awaitable) { 41 | /* Run the async awaitable only when all other async calls registered 42 | * here have completed (or thrown). The bucket argument is a hashable 43 | * key representing the task queue to use. */ 44 | if (!awaitable.name) { 45 | // Make debuging easier by adding a name to this function. 46 | Object.defineProperty(awaitable, 'name', { writable: true }); 47 | if (typeof bucket === 'string') { 48 | awaitable.name = bucket; 49 | } else { 50 | console.warn("Unhandled bucket type (for naming):", typeof bucket, bucket); 51 | } 52 | } 53 | let inactive; 54 | if (!_queueAsyncBuckets.has(bucket)) { 55 | _queueAsyncBuckets.set(bucket, []); 56 | inactive = true; 57 | } 58 | const queue = _queueAsyncBuckets.get(bucket); 59 | const job = new Promise((resolve, reject) => queue.push({ 60 | awaitable, 61 | resolve, 62 | reject 63 | })); 64 | if (inactive) { 65 | /* An executor is not currently active; Start one now. */ 66 | _asyncQueueExecutor(queue, () => _queueAsyncBuckets.delete(bucket)); 67 | } 68 | return job; 69 | }; -------------------------------------------------------------------------------- /WASignalGroup/readme.md: -------------------------------------------------------------------------------- 1 | # Signal-Group 2 | 3 | This contains the code to decrypt/encrypt WA group messages. 4 | Originally from [pokearaujo/libsignal-node](https://github.com/pokearaujo/libsignal-node) 5 | 6 | The code has been moved outside the signal package as I felt it didn't belong in ths signal package, as it isn't inherently a part of signal but of WA. -------------------------------------------------------------------------------- /WASignalGroup/sender_chain_key.js: -------------------------------------------------------------------------------- 1 | const SenderMessageKey = require('./sender_message_key'); 2 | //const HKDF = require('./hkdf'); 3 | const crypto = require('libsignal/src/crypto'); 4 | 5 | class SenderChainKey { 6 | MESSAGE_KEY_SEED = Buffer.from([0x01]); 7 | 8 | CHAIN_KEY_SEED = Buffer.from([0x02]); 9 | 10 | iteration = 0; 11 | 12 | chainKey = Buffer.alloc(0); 13 | 14 | constructor(iteration, chainKey) { 15 | this.iteration = iteration; 16 | this.chainKey = chainKey; 17 | } 18 | 19 | getIteration() { 20 | return this.iteration; 21 | } 22 | 23 | getSenderMessageKey() { 24 | return new SenderMessageKey( 25 | this.iteration, 26 | this.getDerivative(this.MESSAGE_KEY_SEED, this.chainKey) 27 | ); 28 | } 29 | 30 | getNext() { 31 | return new SenderChainKey( 32 | this.iteration + 1, 33 | this.getDerivative(this.CHAIN_KEY_SEED, this.chainKey) 34 | ); 35 | } 36 | 37 | getSeed() { 38 | return typeof this.chainKey === 'string' ? Buffer.from(this.chainKey, 'base64') : this.chainKey; 39 | } 40 | 41 | getDerivative(seed, key) { 42 | key = typeof key === 'string' ? Buffer.from(key, 'base64') : key; 43 | const hash = crypto.calculateMAC(key, seed); 44 | //const hash = new Hash().hmac_hash(key, seed, 'sha256', ''); 45 | 46 | return hash; 47 | } 48 | } 49 | 50 | module.exports = SenderChainKey; -------------------------------------------------------------------------------- /WASignalGroup/sender_key_distribution_message.js: -------------------------------------------------------------------------------- 1 | const CiphertextMessage = require('./ciphertext_message'); 2 | const protobufs = require('./protobufs'); 3 | 4 | class SenderKeyDistributionMessage extends CiphertextMessage { 5 | constructor( 6 | id = null, 7 | iteration = null, 8 | chainKey = null, 9 | signatureKey = null, 10 | serialized = null 11 | ) { 12 | super(); 13 | if (serialized) { 14 | try { 15 | const version = serialized[0]; 16 | const message = serialized.slice(1); 17 | 18 | const distributionMessage = protobufs.SenderKeyDistributionMessage.decode( 19 | message 20 | ).toJSON(); 21 | this.serialized = serialized; 22 | this.id = distributionMessage.id; 23 | this.iteration = distributionMessage.iteration; 24 | this.chainKey = distributionMessage.chainKey; 25 | this.signatureKey = distributionMessage.signingKey; 26 | } catch (e) { 27 | throw new Error(e); 28 | } 29 | } else { 30 | const version = this.intsToByteHighAndLow(this.CURRENT_VERSION, this.CURRENT_VERSION); 31 | this.id = id; 32 | this.iteration = iteration; 33 | this.chainKey = chainKey; 34 | this.signatureKey = signatureKey; 35 | const message = protobufs.SenderKeyDistributionMessage.encode( 36 | protobufs.SenderKeyDistributionMessage.create({ 37 | id, 38 | iteration, 39 | chainKey, 40 | signingKey: this.signatureKey, 41 | }) 42 | ).finish(); 43 | this.serialized = Buffer.concat([Buffer.from([version]), message]); 44 | } 45 | } 46 | 47 | intsToByteHighAndLow(highValue, lowValue) { 48 | return (((highValue << 4) | lowValue) & 0xff) % 256; 49 | } 50 | 51 | serialize() { 52 | return this.serialized; 53 | } 54 | 55 | getType() { 56 | return this.SENDERKEY_DISTRIBUTION_TYPE; 57 | } 58 | 59 | getIteration() { 60 | return this.iteration; 61 | } 62 | 63 | getChainKey() { 64 | return typeof this.chainKey === 'string' ? Buffer.from(this.chainKey, 'base64') : this.chainKey; 65 | } 66 | 67 | getSignatureKey() { 68 | return typeof this.signatureKey === 'string' 69 | ? Buffer.from(this.signatureKey, 'base64') 70 | : this.signatureKey; 71 | } 72 | 73 | getId() { 74 | return this.id; 75 | } 76 | } 77 | 78 | module.exports = SenderKeyDistributionMessage; -------------------------------------------------------------------------------- /WASignalGroup/sender_key_message.js: -------------------------------------------------------------------------------- 1 | const CiphertextMessage = require('./ciphertext_message'); 2 | const curve = require('libsignal/src/curve'); 3 | const protobufs = require('./protobufs'); 4 | 5 | class SenderKeyMessage extends CiphertextMessage { 6 | SIGNATURE_LENGTH = 64; 7 | 8 | constructor( 9 | keyId = null, 10 | iteration = null, 11 | ciphertext = null, 12 | signatureKey = null, 13 | serialized = null 14 | ) { 15 | super(); 16 | if (serialized) { 17 | const version = serialized[0]; 18 | const message = serialized.slice(1, serialized.length - this.SIGNATURE_LENGTH); 19 | const signature = serialized.slice(-1 * this.SIGNATURE_LENGTH); 20 | const senderKeyMessage = protobufs.SenderKeyMessage.decode(message).toJSON(); 21 | senderKeyMessage.ciphertext = Buffer.from(senderKeyMessage.ciphertext, 'base64'); 22 | 23 | this.serialized = serialized; 24 | this.messageVersion = (version & 0xff) >> 4; 25 | 26 | this.keyId = senderKeyMessage.id; 27 | this.iteration = senderKeyMessage.iteration; 28 | this.ciphertext = senderKeyMessage.ciphertext; 29 | this.signature = signature; 30 | } else { 31 | const version = (((this.CURRENT_VERSION << 4) | this.CURRENT_VERSION) & 0xff) % 256; 32 | ciphertext = Buffer.from(ciphertext); // .toString('base64'); 33 | const message = protobufs.SenderKeyMessage.encode( 34 | protobufs.SenderKeyMessage.create({ 35 | id: keyId, 36 | iteration, 37 | ciphertext, 38 | }) 39 | ).finish(); 40 | 41 | const signature = this.getSignature( 42 | signatureKey, 43 | Buffer.concat([Buffer.from([version]), message]) 44 | ); 45 | this.serialized = Buffer.concat([Buffer.from([version]), message, Buffer.from(signature)]); 46 | this.messageVersion = this.CURRENT_VERSION; 47 | this.keyId = keyId; 48 | this.iteration = iteration; 49 | this.ciphertext = ciphertext; 50 | this.signature = signature; 51 | } 52 | } 53 | 54 | getKeyId() { 55 | return this.keyId; 56 | } 57 | 58 | getIteration() { 59 | return this.iteration; 60 | } 61 | 62 | getCipherText() { 63 | return this.ciphertext; 64 | } 65 | 66 | verifySignature(signatureKey) { 67 | const part1 = this.serialized.slice(0, this.serialized.length - this.SIGNATURE_LENGTH); 68 | const part2 = this.serialized.slice(-1 * this.SIGNATURE_LENGTH); 69 | const res = curve.verifySignature(signatureKey, part1, part2); 70 | if (!res) throw new Error('Invalid signature!'); 71 | } 72 | 73 | getSignature(signatureKey, serialized) { 74 | const signature = Buffer.from( 75 | curve.calculateSignature( 76 | signatureKey, 77 | serialized 78 | ) 79 | ); 80 | return signature; 81 | } 82 | 83 | serialize() { 84 | return this.serialized; 85 | } 86 | 87 | getType() { 88 | return 4; 89 | } 90 | } 91 | 92 | module.exports = SenderKeyMessage; -------------------------------------------------------------------------------- /WASignalGroup/sender_key_name.js: -------------------------------------------------------------------------------- 1 | function isNull(str) { 2 | return str === null || str.value === ''; 3 | } 4 | 5 | /** 6 | * java String hashCode 的实现 7 | * @param strKey 8 | * @return intValue 9 | */ 10 | function intValue(num) { 11 | const MAX_VALUE = 0x7fffffff; 12 | const MIN_VALUE = -0x80000000; 13 | if (num > MAX_VALUE || num < MIN_VALUE) { 14 | // eslint-disable-next-line 15 | return (num &= 0xffffffff); 16 | } 17 | return num; 18 | } 19 | 20 | function hashCode(strKey) { 21 | let hash = 0; 22 | if (!isNull(strKey)) { 23 | for (let i = 0; i < strKey.length; i++) { 24 | hash = hash * 31 + strKey.charCodeAt(i); 25 | hash = intValue(hash); 26 | } 27 | } 28 | return hash; 29 | } 30 | 31 | /** 32 | * 将js页面的number类型转换为java的int类型 33 | * @param num 34 | * @return intValue 35 | */ 36 | 37 | class SenderKeyName { 38 | constructor(groupId, sender) { 39 | this.groupId = groupId; 40 | this.sender = sender; 41 | } 42 | 43 | getGroupId() { 44 | return this.groupId; 45 | } 46 | 47 | getSender() { 48 | return this.sender; 49 | } 50 | 51 | serialize() { 52 | return `${this.groupId}::${this.sender.id}::${this.sender.deviceId}`; 53 | } 54 | 55 | toString() { 56 | return this.serialize(); 57 | } 58 | 59 | equals(other) { 60 | if (other === null) return false; 61 | if (!(other instanceof SenderKeyName)) return false; 62 | return this.groupId === other.groupId && this.sender.toString() === other.sender.toString(); 63 | } 64 | 65 | hashCode() { 66 | return hashCode(this.groupId) ^ hashCode(this.sender.toString()); 67 | } 68 | } 69 | 70 | module.exports = SenderKeyName; -------------------------------------------------------------------------------- /WASignalGroup/sender_key_record.js: -------------------------------------------------------------------------------- 1 | const SenderKeyState = require('./sender_key_state'); 2 | 3 | class SenderKeyRecord { 4 | MAX_STATES = 5; 5 | 6 | constructor(serialized) { 7 | this.senderKeyStates = []; 8 | 9 | if (serialized) { 10 | const list = serialized; 11 | for (let i = 0; i < list.length; i++) { 12 | const structure = list[i]; 13 | this.senderKeyStates.push( 14 | new SenderKeyState(null, null, null, null, null, null, structure) 15 | ); 16 | } 17 | } 18 | } 19 | 20 | isEmpty() { 21 | return this.senderKeyStates.length === 0; 22 | } 23 | 24 | getSenderKeyState(keyId) { 25 | if (!keyId && this.senderKeyStates.length) return this.senderKeyStates[this.senderKeyStates.length - 1]; 26 | for (let i = 0; i < this.senderKeyStates.length; i++) { 27 | const state = this.senderKeyStates[i]; 28 | if (state.getKeyId() === keyId) { 29 | return state; 30 | } 31 | } 32 | } 33 | 34 | addSenderKeyState(id, iteration, chainKey, signatureKey) { 35 | this.senderKeyStates.push(new SenderKeyState(id, iteration, chainKey, null, signatureKey)); 36 | if (this.senderKeyStates.length > 5) { 37 | this.senderKeyStates.shift() 38 | } 39 | } 40 | 41 | setSenderKeyState(id, iteration, chainKey, keyPair) { 42 | this.senderKeyStates.length = 0; 43 | this.senderKeyStates.push(new SenderKeyState(id, iteration, chainKey, keyPair)); 44 | } 45 | 46 | serialize() { 47 | const recordStructure = []; 48 | for (let i = 0; i < this.senderKeyStates.length; i++) { 49 | const senderKeyState = this.senderKeyStates[i]; 50 | recordStructure.push(senderKeyState.getStructure()); 51 | } 52 | return recordStructure; 53 | } 54 | } 55 | 56 | module.exports = SenderKeyRecord; -------------------------------------------------------------------------------- /WASignalGroup/sender_key_state.js: -------------------------------------------------------------------------------- 1 | const SenderChainKey = require('./sender_chain_key'); 2 | const SenderMessageKey = require('./sender_message_key'); 3 | 4 | const protobufs = require('./protobufs'); 5 | 6 | class SenderKeyState { 7 | MAX_MESSAGE_KEYS = 2000; 8 | 9 | constructor( 10 | id = null, 11 | iteration = null, 12 | chainKey = null, 13 | signatureKeyPair = null, 14 | signatureKeyPublic = null, 15 | signatureKeyPrivate = null, 16 | senderKeyStateStructure = null 17 | ) { 18 | if (senderKeyStateStructure) { 19 | this.senderKeyStateStructure = senderKeyStateStructure; 20 | } else { 21 | if (signatureKeyPair) { 22 | signatureKeyPublic = signatureKeyPair.public; 23 | signatureKeyPrivate = signatureKeyPair.private; 24 | } 25 | 26 | chainKey = typeof chainKey === 'string' ? Buffer.from(chainKey, 'base64') : chainKey; 27 | this.senderKeyStateStructure = protobufs.SenderKeyStateStructure.create(); 28 | const senderChainKeyStructure = protobufs.SenderChainKey.create(); 29 | senderChainKeyStructure.iteration = iteration; 30 | senderChainKeyStructure.seed = chainKey; 31 | this.senderKeyStateStructure.senderChainKey = senderChainKeyStructure; 32 | 33 | const signingKeyStructure = protobufs.SenderSigningKey.create(); 34 | signingKeyStructure.public = 35 | typeof signatureKeyPublic === 'string' ? 36 | Buffer.from(signatureKeyPublic, 'base64') : 37 | signatureKeyPublic; 38 | if (signatureKeyPrivate) { 39 | signingKeyStructure.private = 40 | typeof signatureKeyPrivate === 'string' ? 41 | Buffer.from(signatureKeyPrivate, 'base64') : 42 | signatureKeyPrivate; 43 | } 44 | this.senderKeyStateStructure.senderKeyId = id; 45 | this.senderChainKey = senderChainKeyStructure; 46 | this.senderKeyStateStructure.senderSigningKey = signingKeyStructure; 47 | } 48 | this.senderKeyStateStructure.senderMessageKeys = 49 | this.senderKeyStateStructure.senderMessageKeys || []; 50 | } 51 | 52 | SenderKeyState(senderKeyStateStructure) { 53 | this.senderKeyStateStructure = senderKeyStateStructure; 54 | } 55 | 56 | getKeyId() { 57 | return this.senderKeyStateStructure.senderKeyId; 58 | } 59 | 60 | getSenderChainKey() { 61 | return new SenderChainKey( 62 | this.senderKeyStateStructure.senderChainKey.iteration, 63 | this.senderKeyStateStructure.senderChainKey.seed 64 | ); 65 | } 66 | 67 | setSenderChainKey(chainKey) { 68 | const senderChainKeyStructure = protobufs.SenderChainKey.create({ 69 | iteration: chainKey.getIteration(), 70 | seed: chainKey.getSeed(), 71 | }); 72 | this.senderKeyStateStructure.senderChainKey = senderChainKeyStructure; 73 | } 74 | 75 | getSigningKeyPublic() { 76 | return typeof this.senderKeyStateStructure.senderSigningKey.public === 'string' ? 77 | Buffer.from(this.senderKeyStateStructure.senderSigningKey.public, 'base64') : 78 | this.senderKeyStateStructure.senderSigningKey.public; 79 | } 80 | 81 | getSigningKeyPrivate() { 82 | return typeof this.senderKeyStateStructure.senderSigningKey.private === 'string' ? 83 | Buffer.from(this.senderKeyStateStructure.senderSigningKey.private, 'base64') : 84 | this.senderKeyStateStructure.senderSigningKey.private; 85 | } 86 | 87 | hasSenderMessageKey(iteration) { 88 | const list = this.senderKeyStateStructure.senderMessageKeys; 89 | for (let o = 0; o < list.length; o++) { 90 | const senderMessageKey = list[o]; 91 | if (senderMessageKey.iteration === iteration) return true; 92 | } 93 | return false; 94 | } 95 | 96 | addSenderMessageKey(senderMessageKey) { 97 | const senderMessageKeyStructure = protobufs.SenderKeyStateStructure.create({ 98 | iteration: senderMessageKey.getIteration(), 99 | seed: senderMessageKey.getSeed(), 100 | }); 101 | this.senderKeyStateStructure.senderMessageKeys.push(senderMessageKeyStructure); 102 | 103 | if (this.senderKeyStateStructure.senderMessageKeys.length > this.MAX_MESSAGE_KEYS) { 104 | this.senderKeyStateStructure.senderMessageKeys.shift(); 105 | } 106 | } 107 | 108 | removeSenderMessageKey(iteration) { 109 | let result = null; 110 | 111 | this.senderKeyStateStructure.senderMessageKeys = this.senderKeyStateStructure.senderMessageKeys.filter( 112 | senderMessageKey => { 113 | if (senderMessageKey.iteration === iteration) result = senderMessageKey; 114 | return senderMessageKey.iteration !== iteration; 115 | } 116 | ); 117 | 118 | if (result != null) { 119 | return new SenderMessageKey(result.iteration, result.seed); 120 | } 121 | return null; 122 | } 123 | 124 | getStructure() { 125 | return this.senderKeyStateStructure; 126 | } 127 | } 128 | 129 | module.exports = SenderKeyState; -------------------------------------------------------------------------------- /WASignalGroup/sender_message_key.js: -------------------------------------------------------------------------------- 1 | const { deriveSecrets } = require('libsignal/src/crypto'); 2 | class SenderMessageKey { 3 | iteration = 0; 4 | 5 | iv = Buffer.alloc(0); 6 | 7 | cipherKey = Buffer.alloc(0); 8 | 9 | seed = Buffer.alloc(0); 10 | 11 | constructor(iteration, seed) { 12 | const derivative = deriveSecrets(seed, Buffer.alloc(32), Buffer.from('WhisperGroup')); 13 | const keys = new Uint8Array(32); 14 | keys.set(new Uint8Array(derivative[0].slice(16))); 15 | keys.set(new Uint8Array(derivative[1].slice(0, 16)), 16); 16 | this.iv = Buffer.from(derivative[0].slice(0, 16)); 17 | this.cipherKey = Buffer.from(keys.buffer); 18 | 19 | this.iteration = iteration; 20 | this.seed = seed; 21 | } 22 | 23 | getIteration() { 24 | return this.iteration; 25 | } 26 | 27 | getIv() { 28 | return this.iv; 29 | } 30 | 31 | getCipherKey() { 32 | return this.cipherKey; 33 | } 34 | 35 | getSeed() { 36 | return this.seed; 37 | } 38 | } 39 | module.exports = SenderMessageKey; -------------------------------------------------------------------------------- /engine-requirements.js: -------------------------------------------------------------------------------- 1 | const major = parseInt(process.versions.node.split('.')[0], 10); 2 | 3 | if (major < 20) { 4 | console.error( 5 | `\n❌ This package requires Node.js 20+ to run reliably.\n` + 6 | ` You are using Node.js ${process.versions.node}.\n` + 7 | ` Please upgrade to Node.js 20+ to proceed.\n` 8 | ); 9 | process.exit(1); 10 | } 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'roots': [ 3 | '/src' 4 | ], 5 | 'testMatch': [ 6 | '**/Tests/test.*.+(ts|tsx|js)', 7 | ], 8 | 'transform': { 9 | '^.+\\.(ts|tsx)$': 'ts-jest' 10 | }, 11 | moduleNameMapper: { 12 | '^axios$': require.resolve('axios'), 13 | }, 14 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baileys", 3 | "version": "6.7.18", 4 | "description": "A WebSockets library for interacting with WhatsApp Web", 5 | "keywords": [ 6 | "whatsapp", 7 | "automation" 8 | ], 9 | "homepage": "https://github.com/WhiskeySockets/Baileys/", 10 | "repository": { 11 | "url": "git@github.com:WhiskeySockets/Baileys.git" 12 | }, 13 | "license": "MIT", 14 | "author": "Rajeh Taher", 15 | "main": "lib/index.js", 16 | "types": "lib/index.d.ts", 17 | "files": [ 18 | "lib/*", 19 | "WAProto/*.ts", 20 | "WAProto/*.js", 21 | "WASignalGroup/*.js", 22 | "engine-requirements.js" 23 | ], 24 | "scripts": { 25 | "build:all": "tsc && typedoc", 26 | "build:docs": "typedoc", 27 | "build:tsc": "tsc", 28 | "changelog:last": "conventional-changelog -p angular -r 2", 29 | "changelog:preview": "conventional-changelog -p angular -u", 30 | "changelog:update": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 31 | "example": "node --inspect -r ts-node/register Example/example.ts", 32 | "gen:protobuf": "sh WAProto/GenerateStatics.sh", 33 | "lint": "eslint src --ext .js,.ts", 34 | "lint:fix": "yarn lint --fix", 35 | "prepack": "tsc", 36 | "prepare": "tsc", 37 | "preinstall": "node ./engine-requirements.js", 38 | "release": "release-it", 39 | "test": "jest" 40 | }, 41 | "dependencies": { 42 | "@cacheable/node-cache": "^1.4.0", 43 | "@hapi/boom": "^9.1.3", 44 | "@whiskeysockets/eslint-config": "github:whiskeysockets/eslint-config", 45 | "async-mutex": "^0.5.0", 46 | "axios": "^1.6.0", 47 | "libsignal": "github:WhiskeySockets/libsignal-node", 48 | "lodash": "^4.17.21", 49 | "music-metadata": "^7.12.3", 50 | "pino": "^9.6", 51 | "protobufjs": "^7.2.4", 52 | "ws": "^8.13.0" 53 | }, 54 | "devDependencies": { 55 | "@types/jest": "^27.5.1", 56 | "@types/node": "^16.0.0", 57 | "@types/ws": "^8.0.0", 58 | "conventional-changelog-cli": "^2.2.2", 59 | "eslint": "^8.0.0", 60 | "jest": "^27.0.6", 61 | "jimp": "^0.16.1", 62 | "json": "^11.0.0", 63 | "link-preview-js": "^3.0.0", 64 | "open": "^8.4.2", 65 | "protobufjs-cli": "^1.1.3", 66 | "release-it": "^15.10.3", 67 | "sharp": "^0.32.6", 68 | "ts-jest": "^27.0.3", 69 | "ts-node": "^10.8.1", 70 | "typedoc": "^0.27.9", 71 | "typedoc-plugin-markdown": "4.4.2", 72 | "typescript": "^5.8.2" 73 | }, 74 | "peerDependencies": { 75 | "audio-decode": "^2.1.3", 76 | "jimp": "^0.16.1", 77 | "link-preview-js": "^3.0.0", 78 | "sharp": "^0.32.6" 79 | }, 80 | "peerDependenciesMeta": { 81 | "audio-decode": { 82 | "optional": true 83 | }, 84 | "jimp": { 85 | "optional": true 86 | }, 87 | "link-preview-js": { 88 | "optional": true 89 | }, 90 | "sharp": { 91 | "optional": true 92 | } 93 | }, 94 | "packageManager": "yarn@1.22.19", 95 | "engines": { 96 | "node": ">=20.0.0" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /proto-extract/README.md: -------------------------------------------------------------------------------- 1 | # Proto Extract 2 | 3 | Derived initially from `whatseow`'s proto extract, this version generates a predictable diff friendly protobuf. It also does not rely on a hardcoded set of modules to look for but finds all proto modules on its own and extracts the proto from there. 4 | 5 | Thanks to [wppconnect-team](https://github.com/wppconnect-team) for the script update to make it work with the latest version of whatsapp. 6 | 7 | ## Usage 8 | 1. Install dependencies with `yarn` (or `npm install`) 9 | 2. `yarn start` 10 | 3. The script will update `../WAProto/WAProto.proto` (except if something is broken) 11 | -------------------------------------------------------------------------------- /proto-extract/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp-web-protobuf-extractor", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "node index.js" 7 | }, 8 | "dependencies": { 9 | "acorn": "^6.4.1", 10 | "acorn-walk": "^6.1.1", 11 | "request": "^2.88.0", 12 | "request-promise-core": "^1.1.2", 13 | "request-promise-native": "^1.0.7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Defaults/baileys-version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": [2, 3000, 1023223821] 3 | } 4 | -------------------------------------------------------------------------------- /src/Defaults/index.ts: -------------------------------------------------------------------------------- 1 | import { proto } from '../../WAProto' 2 | import { makeLibSignalRepository } from '../Signal/libsignal' 3 | import type { AuthenticationState, MediaType, SocketConfig, WAVersion } from '../Types' 4 | import { Browsers } from '../Utils' 5 | import logger from '../Utils/logger' 6 | import { version } from './baileys-version.json' 7 | 8 | export const UNAUTHORIZED_CODES = [401, 403, 419] 9 | 10 | export const DEFAULT_ORIGIN = 'https://web.whatsapp.com' 11 | export const DEF_CALLBACK_PREFIX = 'CB:' 12 | export const DEF_TAG_PREFIX = 'TAG:' 13 | export const PHONE_CONNECTION_CB = 'CB:Pong' 14 | 15 | export const WA_DEFAULT_EPHEMERAL = 7 * 24 * 60 * 60 16 | 17 | export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0' 18 | export const DICT_VERSION = 2 19 | export const KEY_BUNDLE_TYPE = Buffer.from([5]) 20 | export const NOISE_WA_HEADER = Buffer.from( 21 | [ 87, 65, 6, DICT_VERSION ] 22 | ) // last is "DICT_VERSION" 23 | /** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */ 24 | export const URL_REGEX = /https:\/\/(?![^:@\/\s]+:[^:@\/\s]+@)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:\d+)?(\/[^\s]*)?/g 25 | 26 | export const WA_CERT_DETAILS = { 27 | SERIAL: 0, 28 | } 29 | 30 | export const PROCESSABLE_HISTORY_TYPES = [ 31 | proto.Message.HistorySyncNotification.HistorySyncType.INITIAL_BOOTSTRAP, 32 | proto.Message.HistorySyncNotification.HistorySyncType.PUSH_NAME, 33 | proto.Message.HistorySyncNotification.HistorySyncType.RECENT, 34 | proto.Message.HistorySyncNotification.HistorySyncType.FULL, 35 | proto.Message.HistorySyncNotification.HistorySyncType.ON_DEMAND, 36 | ] 37 | 38 | export const DEFAULT_CONNECTION_CONFIG: SocketConfig = { 39 | version: version as WAVersion, 40 | browser: Browsers.ubuntu('Chrome'), 41 | waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat', 42 | connectTimeoutMs: 20_000, 43 | keepAliveIntervalMs: 30_000, 44 | logger: logger.child({ class: 'baileys' }), 45 | emitOwnEvents: true, 46 | defaultQueryTimeoutMs: 60_000, 47 | customUploadHosts: [], 48 | retryRequestDelayMs: 250, 49 | maxMsgRetryCount: 5, 50 | fireInitQueries: true, 51 | auth: undefined as unknown as AuthenticationState, 52 | markOnlineOnConnect: true, 53 | syncFullHistory: false, 54 | patchMessageBeforeSending: msg => msg, 55 | shouldSyncHistoryMessage: () => true, 56 | shouldIgnoreJid: () => false, 57 | linkPreviewImageThumbnailWidth: 192, 58 | transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 }, 59 | generateHighQualityLinkPreview: false, 60 | options: { }, 61 | appStateMacVerification: { 62 | patch: false, 63 | snapshot: false, 64 | }, 65 | countryCode: 'US', 66 | getMessage: async() => undefined, 67 | cachedGroupMetadata: async() => undefined, 68 | makeSignalRepository: makeLibSignalRepository 69 | } 70 | 71 | export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = { 72 | image: '/mms/image', 73 | video: '/mms/video', 74 | document: '/mms/document', 75 | audio: '/mms/audio', 76 | sticker: '/mms/image', 77 | 'thumbnail-link': '/mms/image', 78 | 'product-catalog-image': '/product/image', 79 | 'md-app-state': '', 80 | 'md-msg-hist': '/mms/md-app-state', 81 | } 82 | 83 | export const MEDIA_HKDF_KEY_MAPPING = { 84 | 'audio': 'Audio', 85 | 'document': 'Document', 86 | 'gif': 'Video', 87 | 'image': 'Image', 88 | 'ppic': '', 89 | 'product': 'Image', 90 | 'ptt': 'Audio', 91 | 'sticker': 'Image', 92 | 'video': 'Video', 93 | 'thumbnail-document': 'Document Thumbnail', 94 | 'thumbnail-image': 'Image Thumbnail', 95 | 'thumbnail-video': 'Video Thumbnail', 96 | 'thumbnail-link': 'Link Thumbnail', 97 | 'md-msg-hist': 'History', 98 | 'md-app-state': 'App State', 99 | 'product-catalog-image': '', 100 | 'payment-bg-image': 'Payment Background', 101 | 'ptv': 'Video' 102 | } 103 | 104 | export const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[] 105 | 106 | export const MIN_PREKEY_COUNT = 5 107 | 108 | export const INITIAL_PREKEY_COUNT = 30 109 | 110 | export const DEFAULT_CACHE_TTLS = { 111 | SIGNAL_STORE: 5 * 60, // 5 minutes 112 | MSG_RETRY: 60 * 60, // 1 hour 113 | CALL_OFFER: 5 * 60, // 5 minutes 114 | USER_DEVICES: 5 * 60, // 5 minutes 115 | } 116 | -------------------------------------------------------------------------------- /src/Signal/libsignal.ts: -------------------------------------------------------------------------------- 1 | import * as libsignal from 'libsignal' 2 | import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage, SenderKeyName, SenderKeyRecord } from '../../WASignalGroup' 3 | import { SignalAuthState } from '../Types' 4 | import { SignalRepository } from '../Types/Signal' 5 | import { generateSignalPubKey } from '../Utils' 6 | import { jidDecode } from '../WABinary' 7 | 8 | export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository { 9 | const storage = signalStorage(auth) 10 | return { 11 | decryptGroupMessage({ group, authorJid, msg }) { 12 | const senderName = jidToSignalSenderKeyName(group, authorJid) 13 | const cipher = new GroupCipher(storage, senderName) 14 | 15 | return cipher.decrypt(msg) 16 | }, 17 | async processSenderKeyDistributionMessage({ item, authorJid }) { 18 | const builder = new GroupSessionBuilder(storage) 19 | const senderName = jidToSignalSenderKeyName(item.groupId!, authorJid) 20 | 21 | const senderMsg = new SenderKeyDistributionMessage(null, null, null, null, item.axolotlSenderKeyDistributionMessage) 22 | const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName]) 23 | if(!senderKey) { 24 | await storage.storeSenderKey(senderName, new SenderKeyRecord()) 25 | } 26 | 27 | await builder.process(senderName, senderMsg) 28 | }, 29 | async decryptMessage({ jid, type, ciphertext }) { 30 | const addr = jidToSignalProtocolAddress(jid) 31 | const session = new libsignal.SessionCipher(storage, addr) 32 | let result: Buffer 33 | switch (type) { 34 | case 'pkmsg': 35 | result = await session.decryptPreKeyWhisperMessage(ciphertext) 36 | break 37 | case 'msg': 38 | result = await session.decryptWhisperMessage(ciphertext) 39 | break 40 | } 41 | 42 | return result 43 | }, 44 | async encryptMessage({ jid, data }) { 45 | const addr = jidToSignalProtocolAddress(jid) 46 | const cipher = new libsignal.SessionCipher(storage, addr) 47 | 48 | const { type: sigType, body } = await cipher.encrypt(data) 49 | const type = sigType === 3 ? 'pkmsg' : 'msg' 50 | return { type, ciphertext: Buffer.from(body, 'binary') } 51 | }, 52 | async encryptGroupMessage({ group, meId, data }) { 53 | const senderName = jidToSignalSenderKeyName(group, meId) 54 | const builder = new GroupSessionBuilder(storage) 55 | 56 | const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName]) 57 | if(!senderKey) { 58 | await storage.storeSenderKey(senderName, new SenderKeyRecord()) 59 | } 60 | 61 | const senderKeyDistributionMessage = await builder.create(senderName) 62 | const session = new GroupCipher(storage, senderName) 63 | const ciphertext = await session.encrypt(data) 64 | 65 | return { 66 | ciphertext, 67 | senderKeyDistributionMessage: senderKeyDistributionMessage.serialize(), 68 | } 69 | }, 70 | async injectE2ESession({ jid, session }) { 71 | const cipher = new libsignal.SessionBuilder(storage, jidToSignalProtocolAddress(jid)) 72 | await cipher.initOutgoing(session) 73 | }, 74 | jidToSignalProtocolAddress(jid) { 75 | return jidToSignalProtocolAddress(jid).toString() 76 | }, 77 | } 78 | } 79 | 80 | const jidToSignalProtocolAddress = (jid: string) => { 81 | const { user, device } = jidDecode(jid)! 82 | return new libsignal.ProtocolAddress(user, device || 0) 83 | } 84 | 85 | const jidToSignalSenderKeyName = (group: string, user: string): string => { 86 | return new SenderKeyName(group, jidToSignalProtocolAddress(user)).toString() 87 | } 88 | 89 | function signalStorage({ creds, keys }: SignalAuthState) { 90 | return { 91 | loadSession: async(id: string) => { 92 | const { [id]: sess } = await keys.get('session', [id]) 93 | if(sess) { 94 | return libsignal.SessionRecord.deserialize(sess) 95 | } 96 | }, 97 | storeSession: async(id, session) => { 98 | await keys.set({ 'session': { [id]: session.serialize() } }) 99 | }, 100 | isTrustedIdentity: () => { 101 | return true 102 | }, 103 | loadPreKey: async(id: number | string) => { 104 | const keyId = id.toString() 105 | const { [keyId]: key } = await keys.get('pre-key', [keyId]) 106 | if(key) { 107 | return { 108 | privKey: Buffer.from(key.private), 109 | pubKey: Buffer.from(key.public) 110 | } 111 | } 112 | }, 113 | removePreKey: (id: number) => keys.set({ 'pre-key': { [id]: null } }), 114 | loadSignedPreKey: () => { 115 | const key = creds.signedPreKey 116 | return { 117 | privKey: Buffer.from(key.keyPair.private), 118 | pubKey: Buffer.from(key.keyPair.public) 119 | } 120 | }, 121 | loadSenderKey: async(keyId: string) => { 122 | const { [keyId]: key } = await keys.get('sender-key', [keyId]) 123 | if(key) { 124 | return new SenderKeyRecord(key) 125 | } 126 | }, 127 | storeSenderKey: async(keyId, key) => { 128 | await keys.set({ 'sender-key': { [keyId]: key.serialize() } }) 129 | }, 130 | getOurRegistrationId: () => ( 131 | creds.registrationId 132 | ), 133 | getOurIdentity: () => { 134 | const { signedIdentityKey } = creds 135 | return { 136 | privKey: Buffer.from(signedIdentityKey.private), 137 | pubKey: generateSignalPubKey(signedIdentityKey.public), 138 | } 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /src/Socket/Client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './websocket' -------------------------------------------------------------------------------- /src/Socket/Client/types.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { URL } from 'url' 3 | import { SocketConfig } from '../../Types' 4 | 5 | export abstract class AbstractSocketClient extends EventEmitter { 6 | abstract get isOpen(): boolean 7 | abstract get isClosed(): boolean 8 | abstract get isClosing(): boolean 9 | abstract get isConnecting(): boolean 10 | 11 | constructor(public url: URL, public config: SocketConfig) { 12 | super() 13 | this.setMaxListeners(0) 14 | } 15 | 16 | abstract connect(): Promise 17 | abstract close(): Promise 18 | abstract send(str: Uint8Array | string, cb?: (err?: Error) => void): boolean; 19 | } -------------------------------------------------------------------------------- /src/Socket/Client/websocket.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws' 2 | import { DEFAULT_ORIGIN } from '../../Defaults' 3 | import { AbstractSocketClient } from './types' 4 | 5 | export class WebSocketClient extends AbstractSocketClient { 6 | 7 | protected socket: WebSocket | null = null 8 | 9 | get isOpen(): boolean { 10 | return this.socket?.readyState === WebSocket.OPEN 11 | } 12 | get isClosed(): boolean { 13 | return this.socket === null || this.socket?.readyState === WebSocket.CLOSED 14 | } 15 | get isClosing(): boolean { 16 | return this.socket === null || this.socket?.readyState === WebSocket.CLOSING 17 | } 18 | get isConnecting(): boolean { 19 | return this.socket?.readyState === WebSocket.CONNECTING 20 | } 21 | 22 | async connect(): Promise { 23 | if(this.socket) { 24 | return 25 | } 26 | 27 | this.socket = new WebSocket(this.url, { 28 | origin: DEFAULT_ORIGIN, 29 | headers: this.config.options?.headers as {}, 30 | handshakeTimeout: this.config.connectTimeoutMs, 31 | timeout: this.config.connectTimeoutMs, 32 | agent: this.config.agent, 33 | }) 34 | 35 | this.socket.setMaxListeners(0) 36 | 37 | const events = ['close', 'error', 'upgrade', 'message', 'open', 'ping', 'pong', 'unexpected-response'] 38 | 39 | for(const event of events) { 40 | this.socket?.on(event, (...args: any[]) => this.emit(event, ...args)) 41 | } 42 | } 43 | 44 | async close(): Promise { 45 | if(!this.socket) { 46 | return 47 | } 48 | 49 | this.socket.close() 50 | this.socket = null 51 | } 52 | send(str: string | Uint8Array, cb?: (err?: Error) => void): boolean { 53 | this.socket?.send(str, cb) 54 | 55 | return Boolean(this.socket) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Socket/business.ts: -------------------------------------------------------------------------------- 1 | import { GetCatalogOptions, ProductCreate, ProductUpdate, SocketConfig } from '../Types' 2 | import { parseCatalogNode, parseCollectionsNode, parseOrderDetailsNode, parseProductNode, toProductNode, uploadingNecessaryImagesOfProduct } from '../Utils/business' 3 | import { BinaryNode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary' 4 | import { getBinaryNodeChild } from '../WABinary/generic-utils' 5 | import { makeMessagesRecvSocket } from './messages-recv' 6 | 7 | export const makeBusinessSocket = (config: SocketConfig) => { 8 | const sock = makeMessagesRecvSocket(config) 9 | const { 10 | authState, 11 | query, 12 | waUploadToServer 13 | } = sock 14 | 15 | const getCatalog = async({ jid, limit, cursor }: GetCatalogOptions) => { 16 | jid = jid || authState.creds.me?.id 17 | jid = jidNormalizedUser(jid) 18 | 19 | const queryParamNodes: BinaryNode[] = [ 20 | { 21 | tag: 'limit', 22 | attrs: { }, 23 | content: Buffer.from((limit || 10).toString()) 24 | }, 25 | { 26 | tag: 'width', 27 | attrs: { }, 28 | content: Buffer.from('100') 29 | }, 30 | { 31 | tag: 'height', 32 | attrs: { }, 33 | content: Buffer.from('100') 34 | }, 35 | ] 36 | 37 | if(cursor) { 38 | queryParamNodes.push({ 39 | tag: 'after', 40 | attrs: { }, 41 | content: cursor 42 | }) 43 | } 44 | 45 | const result = await query({ 46 | tag: 'iq', 47 | attrs: { 48 | to: S_WHATSAPP_NET, 49 | type: 'get', 50 | xmlns: 'w:biz:catalog' 51 | }, 52 | content: [ 53 | { 54 | tag: 'product_catalog', 55 | attrs: { 56 | jid, 57 | 'allow_shop_source': 'true' 58 | }, 59 | content: queryParamNodes 60 | } 61 | ] 62 | }) 63 | return parseCatalogNode(result) 64 | } 65 | 66 | const getCollections = async(jid?: string, limit = 51) => { 67 | jid = jid || authState.creds.me?.id 68 | jid = jidNormalizedUser(jid) 69 | const result = await query({ 70 | tag: 'iq', 71 | attrs: { 72 | to: S_WHATSAPP_NET, 73 | type: 'get', 74 | xmlns: 'w:biz:catalog', 75 | 'smax_id': '35' 76 | }, 77 | content: [ 78 | { 79 | tag: 'collections', 80 | attrs: { 81 | 'biz_jid': jid, 82 | }, 83 | content: [ 84 | { 85 | tag: 'collection_limit', 86 | attrs: { }, 87 | content: Buffer.from(limit.toString()) 88 | }, 89 | { 90 | tag: 'item_limit', 91 | attrs: { }, 92 | content: Buffer.from(limit.toString()) 93 | }, 94 | { 95 | tag: 'width', 96 | attrs: { }, 97 | content: Buffer.from('100') 98 | }, 99 | { 100 | tag: 'height', 101 | attrs: { }, 102 | content: Buffer.from('100') 103 | } 104 | ] 105 | } 106 | ] 107 | }) 108 | 109 | return parseCollectionsNode(result) 110 | } 111 | 112 | const getOrderDetails = async(orderId: string, tokenBase64: string) => { 113 | const result = await query({ 114 | tag: 'iq', 115 | attrs: { 116 | to: S_WHATSAPP_NET, 117 | type: 'get', 118 | xmlns: 'fb:thrift_iq', 119 | 'smax_id': '5' 120 | }, 121 | content: [ 122 | { 123 | tag: 'order', 124 | attrs: { 125 | op: 'get', 126 | id: orderId 127 | }, 128 | content: [ 129 | { 130 | tag: 'image_dimensions', 131 | attrs: { }, 132 | content: [ 133 | { 134 | tag: 'width', 135 | attrs: { }, 136 | content: Buffer.from('100') 137 | }, 138 | { 139 | tag: 'height', 140 | attrs: { }, 141 | content: Buffer.from('100') 142 | } 143 | ] 144 | }, 145 | { 146 | tag: 'token', 147 | attrs: { }, 148 | content: Buffer.from(tokenBase64) 149 | } 150 | ] 151 | } 152 | ] 153 | }) 154 | 155 | return parseOrderDetailsNode(result) 156 | } 157 | 158 | const productUpdate = async(productId: string, update: ProductUpdate) => { 159 | update = await uploadingNecessaryImagesOfProduct(update, waUploadToServer) 160 | const editNode = toProductNode(productId, update) 161 | 162 | const result = await query({ 163 | tag: 'iq', 164 | attrs: { 165 | to: S_WHATSAPP_NET, 166 | type: 'set', 167 | xmlns: 'w:biz:catalog' 168 | }, 169 | content: [ 170 | { 171 | tag: 'product_catalog_edit', 172 | attrs: { v: '1' }, 173 | content: [ 174 | editNode, 175 | { 176 | tag: 'width', 177 | attrs: { }, 178 | content: '100' 179 | }, 180 | { 181 | tag: 'height', 182 | attrs: { }, 183 | content: '100' 184 | } 185 | ] 186 | } 187 | ] 188 | }) 189 | 190 | const productCatalogEditNode = getBinaryNodeChild(result, 'product_catalog_edit') 191 | const productNode = getBinaryNodeChild(productCatalogEditNode, 'product') 192 | 193 | return parseProductNode(productNode!) 194 | } 195 | 196 | const productCreate = async(create: ProductCreate) => { 197 | // ensure isHidden is defined 198 | create.isHidden = !!create.isHidden 199 | create = await uploadingNecessaryImagesOfProduct(create, waUploadToServer) 200 | const createNode = toProductNode(undefined, create) 201 | 202 | const result = await query({ 203 | tag: 'iq', 204 | attrs: { 205 | to: S_WHATSAPP_NET, 206 | type: 'set', 207 | xmlns: 'w:biz:catalog' 208 | }, 209 | content: [ 210 | { 211 | tag: 'product_catalog_add', 212 | attrs: { v: '1' }, 213 | content: [ 214 | createNode, 215 | { 216 | tag: 'width', 217 | attrs: { }, 218 | content: '100' 219 | }, 220 | { 221 | tag: 'height', 222 | attrs: { }, 223 | content: '100' 224 | } 225 | ] 226 | } 227 | ] 228 | }) 229 | 230 | const productCatalogAddNode = getBinaryNodeChild(result, 'product_catalog_add') 231 | const productNode = getBinaryNodeChild(productCatalogAddNode, 'product') 232 | 233 | return parseProductNode(productNode!) 234 | } 235 | 236 | const productDelete = async(productIds: string[]) => { 237 | const result = await query({ 238 | tag: 'iq', 239 | attrs: { 240 | to: S_WHATSAPP_NET, 241 | type: 'set', 242 | xmlns: 'w:biz:catalog' 243 | }, 244 | content: [ 245 | { 246 | tag: 'product_catalog_delete', 247 | attrs: { v: '1' }, 248 | content: productIds.map( 249 | id => ({ 250 | tag: 'product', 251 | attrs: { }, 252 | content: [ 253 | { 254 | tag: 'id', 255 | attrs: { }, 256 | content: Buffer.from(id) 257 | } 258 | ] 259 | }) 260 | ) 261 | } 262 | ] 263 | }) 264 | 265 | const productCatalogDelNode = getBinaryNodeChild(result, 'product_catalog_delete') 266 | return { 267 | deleted: +(productCatalogDelNode?.attrs.deleted_count || 0) 268 | } 269 | } 270 | 271 | return { 272 | ...sock, 273 | logger: config.logger, 274 | getOrderDetails, 275 | getCatalog, 276 | getCollections, 277 | productCreate, 278 | productDelete, 279 | productUpdate 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/Socket/index.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_CONNECTION_CONFIG } from '../Defaults' 2 | import { UserFacingSocketConfig } from '../Types' 3 | import { makeBusinessSocket } from './business' 4 | 5 | // export the last socket layer 6 | const makeWASocket = (config: UserFacingSocketConfig) => ( 7 | makeBusinessSocket({ 8 | ...DEFAULT_CONNECTION_CONFIG, 9 | ...config 10 | }) 11 | ) 12 | 13 | export default makeWASocket -------------------------------------------------------------------------------- /src/Socket/usync.ts: -------------------------------------------------------------------------------- 1 | import { Boom } from '@hapi/boom' 2 | import { SocketConfig } from '../Types' 3 | import { BinaryNode, S_WHATSAPP_NET } from '../WABinary' 4 | import { USyncQuery } from '../WAUSync' 5 | import { makeSocket } from './socket' 6 | 7 | export const makeUSyncSocket = (config: SocketConfig) => { 8 | const sock = makeSocket(config) 9 | 10 | const { 11 | generateMessageTag, 12 | query, 13 | } = sock 14 | 15 | const executeUSyncQuery = async(usyncQuery: USyncQuery) => { 16 | if(usyncQuery.protocols.length === 0) { 17 | throw new Boom('USyncQuery must have at least one protocol') 18 | } 19 | 20 | // todo: validate users, throw WARNING on no valid users 21 | // variable below has only validated users 22 | const validUsers = usyncQuery.users 23 | 24 | const userNodes = validUsers.map((user) => { 25 | return { 26 | tag: 'user', 27 | attrs: { 28 | jid: !user.phone ? user.id : undefined, 29 | }, 30 | content: usyncQuery.protocols 31 | .map((a) => a.getUserElement(user)) 32 | .filter(a => a !== null) 33 | } as BinaryNode 34 | }) 35 | 36 | const listNode: BinaryNode = { 37 | tag: 'list', 38 | attrs: {}, 39 | content: userNodes 40 | } 41 | 42 | const queryNode: BinaryNode = { 43 | tag: 'query', 44 | attrs: {}, 45 | content: usyncQuery.protocols.map((a) => a.getQueryElement()) 46 | } 47 | const iq = { 48 | tag: 'iq', 49 | attrs: { 50 | to: S_WHATSAPP_NET, 51 | type: 'get', 52 | xmlns: 'usync', 53 | }, 54 | content: [ 55 | { 56 | tag: 'usync', 57 | attrs: { 58 | context: usyncQuery.context, 59 | mode: usyncQuery.mode, 60 | sid: generateMessageTag(), 61 | last: 'true', 62 | index: '0', 63 | }, 64 | content: [ 65 | queryNode, 66 | listNode 67 | ] 68 | } 69 | ], 70 | } 71 | 72 | const result = await query(iq) 73 | 74 | return usyncQuery.parseUSyncQueryResult(result) 75 | } 76 | 77 | return { 78 | ...sock, 79 | executeUSyncQuery, 80 | } 81 | } -------------------------------------------------------------------------------- /src/Tests/test.app-state-sync.ts: -------------------------------------------------------------------------------- 1 | import { AccountSettings, ChatMutation, Contact, InitialAppStateSyncOptions } from '../Types' 2 | import { unixTimestampSeconds } from '../Utils' 3 | import { processSyncAction } from '../Utils/chat-utils' 4 | import logger from '../Utils/logger' 5 | 6 | describe('App State Sync Tests', () => { 7 | 8 | const me: Contact = { id: randomJid() } 9 | // case when initial sync is off 10 | it('should return archive=false event', () => { 11 | const jid = randomJid() 12 | const index = ['archive', jid] 13 | 14 | const CASES: ChatMutation[][] = [ 15 | [ 16 | { 17 | index, 18 | syncAction: { 19 | value: { 20 | archiveChatAction: { 21 | archived: false, 22 | messageRange: { 23 | lastMessageTimestamp: unixTimestampSeconds() 24 | } 25 | } 26 | } 27 | } 28 | } 29 | ], 30 | [ 31 | { 32 | index, 33 | syncAction: { 34 | value: { 35 | archiveChatAction: { 36 | archived: true, 37 | messageRange: { 38 | lastMessageTimestamp: unixTimestampSeconds() 39 | } 40 | } 41 | } 42 | } 43 | }, 44 | { 45 | index, 46 | syncAction: { 47 | value: { 48 | archiveChatAction: { 49 | archived: false, 50 | messageRange: { 51 | lastMessageTimestamp: unixTimestampSeconds() 52 | } 53 | } 54 | } 55 | } 56 | } 57 | ] 58 | ] 59 | 60 | for(const mutations of CASES) { 61 | const events = processSyncAction(mutations, me, undefined, logger) 62 | expect(events['chats.update']).toHaveLength(1) 63 | const event = events['chats.update']?.[0] 64 | expect(event.archive).toEqual(false) 65 | } 66 | }) 67 | // case when initial sync is on 68 | // and unarchiveChats = true 69 | it('should not fire any archive event', () => { 70 | const jid = randomJid() 71 | const index = ['archive', jid] 72 | const now = unixTimestampSeconds() 73 | 74 | const CASES: ChatMutation[][] = [ 75 | [ 76 | { 77 | index, 78 | syncAction: { 79 | value: { 80 | archiveChatAction: { 81 | archived: true, 82 | messageRange: { 83 | lastMessageTimestamp: now - 1 84 | } 85 | } 86 | } 87 | } 88 | } 89 | ], 90 | [ 91 | { 92 | index, 93 | syncAction: { 94 | value: { 95 | archiveChatAction: { 96 | archived: false, 97 | messageRange: { 98 | lastMessageTimestamp: now + 10 99 | } 100 | } 101 | } 102 | } 103 | } 104 | ], 105 | [ 106 | { 107 | index, 108 | syncAction: { 109 | value: { 110 | archiveChatAction: { 111 | archived: true, 112 | messageRange: { 113 | lastMessageTimestamp: now + 10 114 | } 115 | } 116 | } 117 | } 118 | }, 119 | { 120 | index, 121 | syncAction: { 122 | value: { 123 | archiveChatAction: { 124 | archived: false, 125 | messageRange: { 126 | lastMessageTimestamp: now + 11 127 | } 128 | } 129 | } 130 | } 131 | } 132 | ], 133 | ] 134 | 135 | const ctx: InitialAppStateSyncOptions = { 136 | recvChats: { 137 | [jid]: { lastMsgRecvTimestamp: now } 138 | }, 139 | accountSettings: { unarchiveChats: true } 140 | } 141 | 142 | for(const mutations of CASES) { 143 | const events = processSyncActions(mutations, me, ctx, logger) 144 | expect(events['chats.update']?.length).toBeFalsy() 145 | } 146 | }) 147 | 148 | // case when initial sync is on 149 | // with unarchiveChats = true & unarchiveChats = false 150 | it('should fire archive=true events', () => { 151 | const jid = randomJid() 152 | const index = ['archive', jid] 153 | const now = unixTimestampSeconds() 154 | 155 | const CASES: { settings: AccountSettings, mutations: ChatMutation[] }[] = [ 156 | { 157 | settings: { unarchiveChats: true }, 158 | mutations: [ 159 | { 160 | index, 161 | syncAction: { 162 | value: { 163 | archiveChatAction: { 164 | archived: true, 165 | messageRange: { 166 | lastMessageTimestamp: now 167 | } 168 | } 169 | } 170 | } 171 | } 172 | ], 173 | }, 174 | { 175 | settings: { unarchiveChats: false }, 176 | mutations: [ 177 | { 178 | index, 179 | syncAction: { 180 | value: { 181 | archiveChatAction: { 182 | archived: true, 183 | messageRange: { 184 | lastMessageTimestamp: now - 10 185 | } 186 | } 187 | } 188 | } 189 | } 190 | ], 191 | } 192 | ] 193 | 194 | for(const { mutations, settings } of CASES) { 195 | const ctx: InitialAppStateSyncOptions = { 196 | recvChats: { 197 | [jid]: { lastMsgRecvTimestamp: now } 198 | }, 199 | accountSettings: settings 200 | } 201 | const events = processSyncActions(mutations, me, ctx, logger) 202 | expect(events['chats.update']).toHaveLength(1) 203 | const event = events['chats.update']?.[0] 204 | expect(event.archive).toEqual(true) 205 | } 206 | }) 207 | }) -------------------------------------------------------------------------------- /src/Tests/test.key-store.ts: -------------------------------------------------------------------------------- 1 | import { addTransactionCapability, delay } from '../Utils' 2 | import logger from '../Utils/logger' 3 | import { makeMockSignalKeyStore } from './utils' 4 | 5 | logger.level = 'trace' 6 | 7 | describe('Key Store w Transaction Tests', () => { 8 | 9 | const rawStore = makeMockSignalKeyStore() 10 | const store = addTransactionCapability( 11 | rawStore, 12 | logger, 13 | { 14 | maxCommitRetries: 1, 15 | delayBetweenTriesMs: 10 16 | } 17 | ) 18 | 19 | it('should use transaction cache when mutated', async() => { 20 | const key = '123' 21 | const value = new Uint8Array(1) 22 | const ogGet = rawStore.get 23 | await store.transaction( 24 | async() => { 25 | await store.set({ 'session': { [key]: value } }) 26 | 27 | rawStore.get = () => { 28 | throw new Error('should not have been called') 29 | } 30 | 31 | const { [key]: stored } = await store.get('session', [key]) 32 | expect(stored).toEqual(new Uint8Array(1)) 33 | } 34 | ) 35 | 36 | rawStore.get = ogGet 37 | }) 38 | 39 | it('should not commit a failed transaction', async() => { 40 | const key = 'abcd' 41 | await expect( 42 | store.transaction( 43 | async() => { 44 | await store.set({ 'session': { [key]: new Uint8Array(1) } }) 45 | throw new Error('fail') 46 | } 47 | ) 48 | ).rejects.toThrowError( 49 | 'fail' 50 | ) 51 | 52 | const { [key]: stored } = await store.get('session', [key]) 53 | expect(stored).toBeUndefined() 54 | }) 55 | 56 | it('should handle overlapping transactions', async() => { 57 | // promise to let transaction 2 58 | // know that transaction 1 has started 59 | let promiseResolve: () => void 60 | const promise = new Promise(resolve => { 61 | promiseResolve = resolve 62 | }) 63 | 64 | store.transaction( 65 | async() => { 66 | await store.set({ 67 | 'session': { 68 | '1': new Uint8Array(1) 69 | } 70 | }) 71 | // wait for the other transaction to start 72 | await delay(5) 73 | // reolve the promise to let the other transaction continue 74 | promiseResolve() 75 | } 76 | ) 77 | 78 | await store.transaction( 79 | async() => { 80 | await promise 81 | await delay(5) 82 | 83 | expect(store.isInTransaction()).toBe(true) 84 | } 85 | ) 86 | 87 | expect(store.isInTransaction()).toBe(false) 88 | // ensure that the transaction were committed 89 | const { ['1']: stored } = await store.get('session', ['1']) 90 | expect(stored).toEqual(new Uint8Array(1)) 91 | }) 92 | }) -------------------------------------------------------------------------------- /src/Tests/test.libsignal.ts: -------------------------------------------------------------------------------- 1 | import { makeLibSignalRepository } from '../Signal/libsignal' 2 | import { SignalAuthState, SignalDataTypeMap } from '../Types' 3 | import { Curve, generateRegistrationId, generateSignalPubKey, signedKeyPair } from '../Utils' 4 | 5 | describe('Signal Tests', () => { 6 | 7 | it('should correctly encrypt/decrypt 1 message', async() => { 8 | const user1 = makeUser() 9 | const user2 = makeUser() 10 | 11 | const msg = Buffer.from('hello there!') 12 | 13 | await prepareForSendingMessage(user1, user2) 14 | 15 | const result = await user1.repository.encryptMessage( 16 | { jid: user2.jid, data: msg } 17 | ) 18 | 19 | const dec = await user2.repository.decryptMessage( 20 | { jid: user1.jid, ...result } 21 | ) 22 | 23 | expect(dec).toEqual(msg) 24 | }) 25 | 26 | it('should correctly override a session', async() => { 27 | const user1 = makeUser() 28 | const user2 = makeUser() 29 | 30 | const msg = Buffer.from('hello there!') 31 | 32 | for(let preKeyId = 2; preKeyId <= 3;preKeyId++) { 33 | await prepareForSendingMessage(user1, user2, preKeyId) 34 | 35 | const result = await user1.repository.encryptMessage( 36 | { jid: user2.jid, data: msg } 37 | ) 38 | 39 | const dec = await user2.repository.decryptMessage( 40 | { jid: user1.jid, ...result } 41 | ) 42 | 43 | expect(dec).toEqual(msg) 44 | } 45 | }) 46 | 47 | it('should correctly encrypt/decrypt multiple messages', async() => { 48 | const user1 = makeUser() 49 | const user2 = makeUser() 50 | 51 | const msg = Buffer.from('hello there!') 52 | 53 | await prepareForSendingMessage(user1, user2) 54 | 55 | for(let i = 0;i < 10;i++) { 56 | const result = await user1.repository.encryptMessage( 57 | { jid: user2.jid, data: msg } 58 | ) 59 | 60 | const dec = await user2.repository.decryptMessage( 61 | { jid: user1.jid, ...result } 62 | ) 63 | 64 | expect(dec).toEqual(msg) 65 | } 66 | }) 67 | 68 | it('should encrypt/decrypt messages from group', async() => { 69 | const groupId = '123456@g.us' 70 | const participants = [...Array(5)].map(makeUser) 71 | 72 | const msg = Buffer.from('hello there!') 73 | 74 | const sender = participants[0] 75 | const enc = await sender.repository.encryptGroupMessage( 76 | { 77 | group: groupId, 78 | meId: sender.jid, 79 | data: msg 80 | } 81 | ) 82 | 83 | for(const participant of participants) { 84 | if(participant === sender) { 85 | continue 86 | } 87 | 88 | await participant.repository.processSenderKeyDistributionMessage( 89 | { 90 | item: { 91 | groupId, 92 | axolotlSenderKeyDistributionMessage: enc.senderKeyDistributionMessage 93 | }, 94 | authorJid: sender.jid 95 | } 96 | ) 97 | 98 | const dec = await participant.repository.decryptGroupMessage( 99 | { 100 | group: groupId, 101 | authorJid: sender.jid, 102 | msg: enc.ciphertext 103 | } 104 | ) 105 | expect(dec).toEqual(msg) 106 | } 107 | }) 108 | }) 109 | 110 | type User = ReturnType 111 | 112 | function makeUser() { 113 | const store = makeTestAuthState() 114 | const jid = `${Math.random().toString().replace('.', '')}@s.whatsapp.net` 115 | const repository = makeLibSignalRepository(store) 116 | return { store, jid, repository } 117 | } 118 | 119 | async function prepareForSendingMessage( 120 | sender: User, 121 | receiver: User, 122 | preKeyId = 2 123 | ) { 124 | const preKey = Curve.generateKeyPair() 125 | await sender.repository.injectE2ESession( 126 | { 127 | jid: receiver.jid, 128 | session: { 129 | registrationId: receiver.store.creds.registrationId, 130 | identityKey: generateSignalPubKey(receiver.store.creds.signedIdentityKey.public), 131 | signedPreKey: { 132 | keyId: receiver.store.creds.signedPreKey.keyId, 133 | publicKey: generateSignalPubKey(receiver.store.creds.signedPreKey.keyPair.public), 134 | signature: receiver.store.creds.signedPreKey.signature, 135 | }, 136 | preKey: { 137 | keyId: preKeyId, 138 | publicKey: generateSignalPubKey(preKey.public), 139 | } 140 | } 141 | } 142 | ) 143 | 144 | await receiver.store.keys.set({ 145 | 'pre-key': { 146 | [preKeyId]: preKey 147 | } 148 | }) 149 | } 150 | 151 | function makeTestAuthState(): SignalAuthState { 152 | const identityKey = Curve.generateKeyPair() 153 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 154 | const store: { [_: string]: any } = {} 155 | return { 156 | creds: { 157 | signedIdentityKey: identityKey, 158 | registrationId: generateRegistrationId(), 159 | signedPreKey: signedKeyPair(identityKey, 1), 160 | }, 161 | keys: { 162 | get(type, ids) { 163 | const data: { [_: string]: SignalDataTypeMap[typeof type] } = { } 164 | for(const id of ids) { 165 | const item = store[getUniqueId(type, id)] 166 | if(typeof item !== 'undefined') { 167 | data[id] = item 168 | } 169 | } 170 | 171 | return data 172 | }, 173 | set(data) { 174 | for(const type in data) { 175 | for(const id in data[type]) { 176 | store[getUniqueId(type, id)] = data[type][id] 177 | } 178 | } 179 | }, 180 | } 181 | } 182 | 183 | function getUniqueId(type: string, id: string) { 184 | return `${type}.${id}` 185 | } 186 | } -------------------------------------------------------------------------------- /src/Tests/test.media-download.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | import { proto } from '../../WAProto' 3 | import { DownloadableMessage, MediaType } from '../Types' 4 | import { downloadContentFromMessage } from '../Utils' 5 | 6 | jest.setTimeout(20_000) 7 | 8 | type TestVector = { 9 | type: MediaType 10 | message: DownloadableMessage 11 | plaintext: Buffer 12 | } 13 | 14 | const TEST_VECTORS: TestVector[] = [ 15 | { 16 | type: 'image', 17 | message: proto.Message.ImageMessage.decode( 18 | Buffer.from( 19 | 'Ck1odHRwczovL21tZy53aGF0c2FwcC5uZXQvZC9mL0FwaHR4WG9fWXZZcDZlUVNSa0tjOHE5d2ozVUpleWdoY3poM3ExX3I0ektnLmVuYxIKaW1hZ2UvanBlZyIgKTuVFyxDc6mTm4GXPlO3Z911Wd8RBeTrPLSWAEdqW8MomcUBQiB7wH5a4nXMKyLOT0A2nFgnnM/DUH8YjQf8QtkCIekaSkogTB+BXKCWDFrmNzozY0DCPn0L4VKd7yG1ZbZwbgRhzVc=', 20 | 'base64' 21 | ) 22 | ), 23 | plaintext: readFileSync('./Media/cat.jpeg') 24 | }, 25 | { 26 | type: 'image', 27 | message: proto.Message.ImageMessage.decode( 28 | Buffer.from( 29 | 'Ck1odHRwczovL21tZy53aGF0c2FwcC5uZXQvZC9mL0Ftb2tnWkphNWF6QWZxa3dVRzc0eUNUdTlGeWpjMmd5akpqcXNmMUFpZEU5LmVuYxIKaW1hZ2UvanBlZyIg8IS5TQzdzcuvcR7F8HMhWnXmlsV+GOo9JE1/t2k+o9Yoz6o6QiA7kDk8j5KOEQC0kDFE1qW7lBBDYhm5z06N3SirfUj3CUog/CjYF8e670D5wUJwWv2B2mKzDEo8IJLStDv76YmtPfs=', 30 | 'base64' 31 | ) 32 | ), 33 | plaintext: readFileSync('./Media/icon.png') 34 | }, 35 | ] 36 | 37 | describe('Media Download Tests', () => { 38 | 39 | it('should download a full encrypted media correctly', async() => { 40 | for(const { type, message, plaintext } of TEST_VECTORS) { 41 | const readPipe = await downloadContentFromMessage(message, type) 42 | 43 | let buffer = Buffer.alloc(0) 44 | for await (const read of readPipe) { 45 | buffer = Buffer.concat([ buffer, read ]) 46 | } 47 | 48 | expect(buffer).toEqual(plaintext) 49 | } 50 | }) 51 | 52 | it('should download an encrypted media correctly piece', async() => { 53 | for(const { type, message, plaintext } of TEST_VECTORS) { 54 | // check all edge cases 55 | const ranges = [ 56 | { startByte: 51, endByte: plaintext.length - 100 }, // random numbers 57 | { startByte: 1024, endByte: 2038 }, // larger random multiples of 16 58 | { startByte: 1, endByte: plaintext.length - 1 } // borders 59 | ] 60 | for(const range of ranges) { 61 | const readPipe = await downloadContentFromMessage(message, type, range) 62 | 63 | let buffer = Buffer.alloc(0) 64 | for await (const read of readPipe) { 65 | buffer = Buffer.concat([ buffer, read ]) 66 | } 67 | 68 | const hex = buffer.toString('hex') 69 | const expectedHex = plaintext.slice(range.startByte || 0, range.endByte || undefined).toString('hex') 70 | expect(hex).toBe(expectedHex) 71 | 72 | console.log('success on ', range) 73 | } 74 | } 75 | }) 76 | }) -------------------------------------------------------------------------------- /src/Tests/test.messages.ts: -------------------------------------------------------------------------------- 1 | import { WAMessageContent } from '../Types' 2 | import { normalizeMessageContent } from '../Utils' 3 | 4 | describe('Messages Tests', () => { 5 | 6 | it('should correctly unwrap messages', () => { 7 | const CONTENT = { imageMessage: { } } 8 | expectRightContent(CONTENT) 9 | expectRightContent({ 10 | ephemeralMessage: { message: CONTENT } 11 | }) 12 | expectRightContent({ 13 | viewOnceMessage: { 14 | message: { 15 | ephemeralMessage: { message: CONTENT } 16 | } 17 | } 18 | }) 19 | expectRightContent({ 20 | viewOnceMessage: { 21 | message: { 22 | viewOnceMessageV2: { 23 | message: { 24 | ephemeralMessage: { message: CONTENT } 25 | } 26 | } 27 | } 28 | } 29 | }) 30 | 31 | function expectRightContent(content: WAMessageContent) { 32 | expect( 33 | normalizeMessageContent(content) 34 | ).toHaveProperty('imageMessage') 35 | } 36 | }) 37 | }) -------------------------------------------------------------------------------- /src/Tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { SignalDataTypeMap, SignalKeyStore } from '../Types' 2 | import { jidEncode } from '../WABinary' 3 | 4 | export function randomJid() { 5 | return jidEncode(Math.floor(Math.random() * 1000000), Math.random() < 0.5 ? 's.whatsapp.net' : 'g.us') 6 | } 7 | 8 | export function makeMockSignalKeyStore(): SignalKeyStore { 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | const store: { [_: string]: any } = {} 11 | 12 | return { 13 | get(type, ids) { 14 | const data: { [_: string]: SignalDataTypeMap[typeof type] } = { } 15 | for(const id of ids) { 16 | const item = store[getUniqueId(type, id)] 17 | if(typeof item !== 'undefined') { 18 | data[id] = item 19 | } 20 | } 21 | 22 | return data 23 | }, 24 | set(data) { 25 | for(const type in data) { 26 | for(const id in data[type]) { 27 | store[getUniqueId(type, id)] = data[type][id] 28 | } 29 | } 30 | }, 31 | } 32 | 33 | function getUniqueId(type: string, id: string) { 34 | return `${type}.${id}` 35 | } 36 | } -------------------------------------------------------------------------------- /src/Types/Auth.ts: -------------------------------------------------------------------------------- 1 | import type { proto } from '../../WAProto' 2 | import type { Contact } from './Contact' 3 | import type { MinimalMessage } from './Message' 4 | 5 | export type KeyPair = { public: Uint8Array, private: Uint8Array } 6 | export type SignedKeyPair = { 7 | keyPair: KeyPair 8 | signature: Uint8Array 9 | keyId: number 10 | timestampS?: number 11 | } 12 | 13 | export type ProtocolAddress = { 14 | name: string // jid 15 | deviceId: number 16 | } 17 | export type SignalIdentity = { 18 | identifier: ProtocolAddress 19 | identifierKey: Uint8Array 20 | } 21 | 22 | export type LTHashState = { 23 | version: number 24 | hash: Buffer 25 | indexValueMap: { 26 | [indexMacBase64: string]: { valueMac: Uint8Array | Buffer } 27 | } 28 | } 29 | 30 | export type SignalCreds = { 31 | readonly signedIdentityKey: KeyPair 32 | readonly signedPreKey: SignedKeyPair 33 | readonly registrationId: number 34 | } 35 | 36 | export type AccountSettings = { 37 | /** unarchive chats when a new message is received */ 38 | unarchiveChats: boolean 39 | /** the default mode to start new conversations with */ 40 | defaultDisappearingMode?: Pick 41 | } 42 | 43 | export type AuthenticationCreds = SignalCreds & { 44 | readonly noiseKey: KeyPair 45 | readonly pairingEphemeralKeyPair: KeyPair 46 | advSecretKey: string 47 | 48 | me?: Contact 49 | account?: proto.IADVSignedDeviceIdentity 50 | signalIdentities?: SignalIdentity[] 51 | myAppStateKeyId?: string 52 | firstUnuploadedPreKeyId: number 53 | nextPreKeyId: number 54 | 55 | lastAccountSyncTimestamp?: number 56 | platform?: string 57 | 58 | processedHistoryMessages: MinimalMessage[] 59 | /** number of times history & app state has been synced */ 60 | accountSyncCounter: number 61 | accountSettings: AccountSettings 62 | registered: boolean 63 | pairingCode: string | undefined 64 | lastPropHash: string | undefined 65 | routingInfo: Buffer | undefined 66 | } 67 | 68 | export type SignalDataTypeMap = { 69 | 'pre-key': KeyPair 70 | 'session': Uint8Array 71 | 'sender-key': Uint8Array 72 | 'sender-key-memory': { [jid: string]: boolean } 73 | 'app-state-sync-key': proto.Message.IAppStateSyncKeyData 74 | 'app-state-sync-version': LTHashState 75 | } 76 | 77 | export type SignalDataSet = { [T in keyof SignalDataTypeMap]?: { [id: string]: SignalDataTypeMap[T] | null } } 78 | 79 | type Awaitable = T | Promise 80 | 81 | export type SignalKeyStore = { 82 | get(type: T, ids: string[]): Awaitable<{ [id: string]: SignalDataTypeMap[T] }> 83 | set(data: SignalDataSet): Awaitable 84 | /** clear all the data in the store */ 85 | clear?(): Awaitable 86 | } 87 | 88 | export type SignalKeyStoreWithTransaction = SignalKeyStore & { 89 | isInTransaction: () => boolean 90 | transaction(exec: () => Promise): Promise 91 | } 92 | 93 | export type TransactionCapabilityOptions = { 94 | maxCommitRetries: number 95 | delayBetweenTriesMs: number 96 | } 97 | 98 | export type SignalAuthState = { 99 | creds: SignalCreds 100 | keys: SignalKeyStore | SignalKeyStoreWithTransaction 101 | } 102 | 103 | export type AuthenticationState = { 104 | creds: AuthenticationCreds 105 | keys: SignalKeyStore 106 | } -------------------------------------------------------------------------------- /src/Types/Call.ts: -------------------------------------------------------------------------------- 1 | 2 | export type WACallUpdateType = 'offer' | 'ringing' | 'timeout' | 'reject' | 'accept' | 'terminate' 3 | 4 | export type WACallEvent = { 5 | chatId: string 6 | from: string 7 | isGroup?: boolean 8 | groupJid?: string 9 | id: string 10 | date: Date 11 | isVideo?: boolean 12 | status: WACallUpdateType 13 | offline: boolean 14 | latencyMs?: number 15 | } 16 | -------------------------------------------------------------------------------- /src/Types/Chat.ts: -------------------------------------------------------------------------------- 1 | import type { proto } from '../../WAProto' 2 | import type { AccountSettings } from './Auth' 3 | import type { BufferedEventData } from './Events' 4 | import type { LabelActionBody } from './Label' 5 | import type { ChatLabelAssociationActionBody } from './LabelAssociation' 6 | import type { MessageLabelAssociationActionBody } from './LabelAssociation' 7 | import type { MinimalMessage, WAMessageKey } from './Message' 8 | 9 | /** privacy settings in WhatsApp Web */ 10 | export type WAPrivacyValue = 'all' | 'contacts' | 'contact_blacklist' | 'none' 11 | 12 | export type WAPrivacyOnlineValue = 'all' | 'match_last_seen' 13 | 14 | export type WAPrivacyGroupAddValue = 'all' | 'contacts' | 'contact_blacklist' 15 | 16 | export type WAReadReceiptsValue = 'all' | 'none' 17 | 18 | export type WAPrivacyCallValue = 'all' | 'known' 19 | 20 | export type WAPrivacyMessagesValue = 'all' | 'contacts' 21 | 22 | /** set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send */ 23 | export type WAPresence = 'unavailable' | 'available' | 'composing' | 'recording' | 'paused' 24 | 25 | export const ALL_WA_PATCH_NAMES = ['critical_block', 'critical_unblock_low', 'regular_high', 'regular_low', 'regular'] as const 26 | 27 | export type WAPatchName = typeof ALL_WA_PATCH_NAMES[number] 28 | 29 | export interface PresenceData { 30 | lastKnownPresence: WAPresence 31 | lastSeen?: number 32 | } 33 | 34 | export type BotListInfo = { 35 | jid: string 36 | personaId: string 37 | } 38 | 39 | export type ChatMutation = { 40 | syncAction: proto.ISyncActionData 41 | index: string[] 42 | } 43 | 44 | export type WAPatchCreate = { 45 | syncAction: proto.ISyncActionValue 46 | index: string[] 47 | type: WAPatchName 48 | apiVersion: number 49 | operation: proto.SyncdMutation.SyncdOperation 50 | } 51 | 52 | export type Chat = proto.IConversation & { 53 | /** unix timestamp of when the last message was received in the chat */ 54 | lastMessageRecvTimestamp?: number 55 | } 56 | 57 | export type ChatUpdate = Partial boolean | undefined 68 | }> 69 | 70 | /** 71 | * the last messages in a chat, sorted reverse-chronologically. That is, the latest message should be first in the chat 72 | * for MD modifications, the last message in the array (i.e. the earlist message) must be the last message recv in the chat 73 | * */ 74 | export type LastMessageList = MinimalMessage[] | proto.SyncActionValue.ISyncActionMessageRange 75 | 76 | export type ChatModification = 77 | { 78 | archive: boolean 79 | lastMessages: LastMessageList 80 | } 81 | | { pushNameSetting: string } 82 | | { pin: boolean } 83 | | { 84 | /** mute for duration, or provide timestamp of mute to remove*/ 85 | mute: number | null 86 | } 87 | | { 88 | clear: boolean 89 | } | { 90 | deleteForMe: { deleteMedia: boolean, key: WAMessageKey, timestamp: number } 91 | } 92 | | { 93 | star: { 94 | messages: { id: string, fromMe?: boolean }[] 95 | star: boolean 96 | } 97 | } 98 | | { 99 | markRead: boolean 100 | lastMessages: LastMessageList 101 | } 102 | | { delete: true, lastMessages: LastMessageList } 103 | // Label 104 | | { addLabel: LabelActionBody } 105 | // Label assosiation 106 | | { addChatLabel: ChatLabelAssociationActionBody } 107 | | { removeChatLabel: ChatLabelAssociationActionBody } 108 | | { addMessageLabel: MessageLabelAssociationActionBody } 109 | | { removeMessageLabel: MessageLabelAssociationActionBody } 110 | 111 | export type InitialReceivedChatsState = { 112 | [jid: string]: { 113 | /** the last message received from the other party */ 114 | lastMsgRecvTimestamp?: number 115 | /** the absolute last message in the chat */ 116 | lastMsgTimestamp: number 117 | } 118 | } 119 | 120 | export type InitialAppStateSyncOptions = { 121 | accountSettings: AccountSettings 122 | } 123 | -------------------------------------------------------------------------------- /src/Types/Contact.ts: -------------------------------------------------------------------------------- 1 | export interface Contact { 2 | id: string 3 | lid?: string 4 | /** name of the contact, you have saved on your WA */ 5 | name?: string 6 | /** name of the contact, the contact has set on their own on WA */ 7 | notify?: string 8 | /** I have no idea */ 9 | verifiedName?: string 10 | // Baileys Added 11 | /** 12 | * Url of the profile picture of the contact 13 | * 14 | * 'changed' => if the profile picture has changed 15 | * null => if the profile picture has not been set (default profile picture) 16 | * any other string => url of the profile picture 17 | */ 18 | imgUrl?: string | null 19 | status?: string 20 | } -------------------------------------------------------------------------------- /src/Types/Events.ts: -------------------------------------------------------------------------------- 1 | import type { Boom } from '@hapi/boom' 2 | import { proto } from '../../WAProto' 3 | import { AuthenticationCreds } from './Auth' 4 | import { WACallEvent } from './Call' 5 | import { Chat, ChatUpdate, PresenceData } from './Chat' 6 | import { Contact } from './Contact' 7 | import { GroupMetadata, ParticipantAction, RequestJoinAction, RequestJoinMethod } from './GroupMetadata' 8 | import { Label } from './Label' 9 | import { LabelAssociation } from './LabelAssociation' 10 | import { MessageUpsertType, MessageUserReceiptUpdate, WAMessage, WAMessageKey, WAMessageUpdate } from './Message' 11 | import { ConnectionState } from './State' 12 | 13 | export type BaileysEventMap = { 14 | /** connection state has been updated -- WS closed, opened, connecting etc. */ 15 | 'connection.update': Partial 16 | /** credentials updated -- some metadata, keys or something */ 17 | 'creds.update': Partial 18 | /** set chats (history sync), everything is reverse chronologically sorted */ 19 | 'messaging-history.set': { 20 | chats: Chat[] 21 | contacts: Contact[] 22 | messages: WAMessage[] 23 | isLatest?: boolean 24 | progress?: number | null 25 | syncType?: proto.HistorySync.HistorySyncType 26 | peerDataRequestSessionId?: string | null 27 | } 28 | /** upsert chats */ 29 | 'chats.upsert': Chat[] 30 | /** update the given chats */ 31 | 'chats.update': ChatUpdate[] 32 | 'chats.phoneNumberShare': {lid: string, jid: string} 33 | /** delete chats with given ID */ 34 | 'chats.delete': string[] 35 | /** presence of contact in a chat updated */ 36 | 'presence.update': { id: string, presences: { [participant: string]: PresenceData } } 37 | 38 | 'contacts.upsert': Contact[] 39 | 'contacts.update': Partial[] 40 | 41 | 'messages.delete': { keys: WAMessageKey[] } | { jid: string, all: true } 42 | 'messages.update': WAMessageUpdate[] 43 | 'messages.media-update': { key: WAMessageKey, media?: { ciphertext: Uint8Array, iv: Uint8Array }, error?: Boom }[] 44 | /** 45 | * add/update the given messages. If they were received while the connection was online, 46 | * the update will have type: "notify" 47 | * if requestId is provided, then the messages was received from the phone due to it being unavailable 48 | * */ 49 | 'messages.upsert': { messages: WAMessage[], type: MessageUpsertType, requestId?: string } 50 | /** message was reacted to. If reaction was removed -- then "reaction.text" will be falsey */ 51 | 'messages.reaction': { key: WAMessageKey, reaction: proto.IReaction }[] 52 | 53 | 'message-receipt.update': MessageUserReceiptUpdate[] 54 | 55 | 'groups.upsert': GroupMetadata[] 56 | 'groups.update': Partial[] 57 | /** apply an action to participants in a group */ 58 | 'group-participants.update': { id: string, author: string, participants: string[], action: ParticipantAction } 59 | 'group.join-request': { id: string, author: string, participant: string, action: RequestJoinAction, method: RequestJoinMethod } 60 | 61 | 'blocklist.set': { blocklist: string[] } 62 | 'blocklist.update': { blocklist: string[], type: 'add' | 'remove' } 63 | 64 | /** Receive an update on a call, including when the call was received, rejected, accepted */ 65 | 'call': WACallEvent[] 66 | 'labels.edit': Label 67 | 'labels.association': { association: LabelAssociation, type: 'add' | 'remove' } 68 | } 69 | 70 | export type BufferedEventData = { 71 | historySets: { 72 | chats: { [jid: string]: Chat } 73 | contacts: { [jid: string]: Contact } 74 | messages: { [uqId: string]: WAMessage } 75 | empty: boolean 76 | isLatest: boolean 77 | progress?: number | null 78 | syncType?: proto.HistorySync.HistorySyncType 79 | peerDataRequestSessionId?: string 80 | } 81 | chatUpserts: { [jid: string]: Chat } 82 | chatUpdates: { [jid: string]: ChatUpdate } 83 | chatDeletes: Set 84 | contactUpserts: { [jid: string]: Contact } 85 | contactUpdates: { [jid: string]: Partial } 86 | messageUpserts: { [key: string]: { type: MessageUpsertType, message: WAMessage } } 87 | messageUpdates: { [key: string]: WAMessageUpdate } 88 | messageDeletes: { [key: string]: WAMessageKey } 89 | messageReactions: { [key: string]: { key: WAMessageKey, reactions: proto.IReaction[] } } 90 | messageReceipts: { [key: string]: { key: WAMessageKey, userReceipt: proto.IUserReceipt[] } } 91 | groupUpdates: { [jid: string]: Partial } 92 | } 93 | 94 | export type BaileysEvent = keyof BaileysEventMap 95 | 96 | export interface BaileysEventEmitter { 97 | on(event: T, listener: (arg: BaileysEventMap[T]) => void): void 98 | off(event: T, listener: (arg: BaileysEventMap[T]) => void): void 99 | removeAllListeners(event: T): void 100 | emit(event: T, arg: BaileysEventMap[T]): boolean 101 | } -------------------------------------------------------------------------------- /src/Types/GroupMetadata.ts: -------------------------------------------------------------------------------- 1 | import { Contact } from './Contact' 2 | 3 | export type GroupParticipant = (Contact & { isAdmin?: boolean, isSuperAdmin?: boolean, admin?: 'admin' | 'superadmin' | null }) 4 | 5 | export type ParticipantAction = 'add' | 'remove' | 'promote' | 'demote' | 'modify' 6 | 7 | export type RequestJoinAction = 'created' | 'revoked' | 'rejected' 8 | 9 | export type RequestJoinMethod = 'invite_link' | 'linked_group_join' | 'non_admin_add' | undefined 10 | 11 | export interface GroupMetadata { 12 | id: string 13 | /** group uses 'lid' or 'pn' to send messages */ 14 | addressingMode: "pn" | "lid" 15 | owner: string | undefined 16 | subject: string 17 | /** group subject owner */ 18 | subjectOwner?: string 19 | /** group subject modification date */ 20 | subjectTime?: number 21 | creation?: number 22 | desc?: string 23 | descOwner?: string 24 | descId?: string 25 | /** if this group is part of a community, it returns the jid of the community to which it belongs */ 26 | linkedParent?: string 27 | /** is set when the group only allows admins to change group settings */ 28 | restrict?: boolean 29 | /** is set when the group only allows admins to write messages */ 30 | announce?: boolean 31 | /** is set when the group also allows members to add participants */ 32 | memberAddMode?: boolean 33 | /** Request approval to join the group */ 34 | joinApprovalMode?: boolean 35 | /** is this a community */ 36 | isCommunity?: boolean 37 | /** is this the announce of a community */ 38 | isCommunityAnnounce?: boolean 39 | /** number of group participants */ 40 | size?: number 41 | // Baileys modified array 42 | participants: GroupParticipant[] 43 | ephemeralDuration?: number 44 | inviteCode?: string 45 | /** the person who added you to group or changed some setting in group */ 46 | author?: string 47 | } 48 | 49 | 50 | export interface WAGroupCreateResponse { 51 | status: number 52 | gid?: string 53 | participants?: [{ [key: string]: {} }] 54 | } 55 | 56 | export interface GroupModificationResponse { 57 | status: number 58 | participants?: { [key: string]: {} } 59 | } 60 | -------------------------------------------------------------------------------- /src/Types/Label.ts: -------------------------------------------------------------------------------- 1 | export interface Label { 2 | /** Label uniq ID */ 3 | id: string 4 | /** Label name */ 5 | name: string 6 | /** Label color ID */ 7 | color: number 8 | /** Is label has been deleted */ 9 | deleted: boolean 10 | /** WhatsApp has 5 predefined labels (New customer, New order & etc) */ 11 | predefinedId?: string 12 | } 13 | 14 | export interface LabelActionBody { 15 | id: string 16 | /** Label name */ 17 | name?: string 18 | /** Label color ID */ 19 | color?: number 20 | /** Is label has been deleted */ 21 | deleted?: boolean 22 | /** WhatsApp has 5 predefined labels (New customer, New order & etc) */ 23 | predefinedId?: number 24 | } 25 | 26 | /** WhatsApp has 20 predefined colors */ 27 | export enum LabelColor { 28 | Color1 = 0, 29 | Color2, 30 | Color3, 31 | Color4, 32 | Color5, 33 | Color6, 34 | Color7, 35 | Color8, 36 | Color9, 37 | Color10, 38 | Color11, 39 | Color12, 40 | Color13, 41 | Color14, 42 | Color15, 43 | Color16, 44 | Color17, 45 | Color18, 46 | Color19, 47 | Color20, 48 | } -------------------------------------------------------------------------------- /src/Types/LabelAssociation.ts: -------------------------------------------------------------------------------- 1 | /** Association type */ 2 | export enum LabelAssociationType { 3 | Chat = 'label_jid', 4 | Message = 'label_message' 5 | } 6 | 7 | export type LabelAssociationTypes = `${LabelAssociationType}` 8 | 9 | /** Association for chat */ 10 | export interface ChatLabelAssociation { 11 | type: LabelAssociationType.Chat 12 | chatId: string 13 | labelId: string 14 | } 15 | 16 | /** Association for message */ 17 | export interface MessageLabelAssociation { 18 | type: LabelAssociationType.Message 19 | chatId: string 20 | messageId: string 21 | labelId: string 22 | } 23 | 24 | export type LabelAssociation = ChatLabelAssociation | MessageLabelAssociation 25 | 26 | /** Body for add/remove chat label association action */ 27 | export interface ChatLabelAssociationActionBody { 28 | labelId: string 29 | } 30 | 31 | /** body for add/remove message label association action */ 32 | export interface MessageLabelAssociationActionBody { 33 | labelId: string 34 | messageId: string 35 | } -------------------------------------------------------------------------------- /src/Types/Product.ts: -------------------------------------------------------------------------------- 1 | import { WAMediaUpload } from './Message' 2 | 3 | export type CatalogResult = { 4 | data: { 5 | paging: { cursors: { before: string, after: string } } 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | data: any[] 8 | } 9 | } 10 | 11 | export type ProductCreateResult = { 12 | data: { product: {} } 13 | } 14 | 15 | export type CatalogStatus = { 16 | status: string 17 | canAppeal: boolean 18 | } 19 | 20 | export type CatalogCollection = { 21 | id: string 22 | name: string 23 | products: Product[] 24 | 25 | status: CatalogStatus 26 | } 27 | 28 | export type ProductAvailability = 'in stock' 29 | 30 | export type ProductBase = { 31 | name: string 32 | retailerId?: string 33 | url?: string 34 | description: string 35 | price: number 36 | currency: string 37 | isHidden?: boolean 38 | } 39 | 40 | export type ProductCreate = ProductBase & { 41 | /** ISO country code for product origin. Set to undefined for no country */ 42 | originCountryCode: string | undefined 43 | /** images of the product */ 44 | images: WAMediaUpload[] 45 | } 46 | 47 | export type ProductUpdate = Omit 48 | 49 | export type Product = ProductBase & { 50 | id: string 51 | imageUrls: { [_: string]: string } 52 | reviewStatus: { [_: string]: string } 53 | availability: ProductAvailability 54 | } 55 | 56 | export type OrderPrice = { 57 | currency: string 58 | total: number 59 | } 60 | 61 | export type OrderProduct = { 62 | id: string 63 | imageUrl: string 64 | name: string 65 | quantity: number 66 | 67 | currency: string 68 | price: number 69 | } 70 | 71 | export type OrderDetails = { 72 | price: OrderPrice 73 | products: OrderProduct[] 74 | } 75 | 76 | export type CatalogCursor = string 77 | 78 | export type GetCatalogOptions = { 79 | /** cursor to start from */ 80 | cursor?: CatalogCursor 81 | /** number of products to fetch */ 82 | limit?: number 83 | 84 | jid?: string 85 | } -------------------------------------------------------------------------------- /src/Types/Signal.ts: -------------------------------------------------------------------------------- 1 | import { proto } from '../../WAProto' 2 | 3 | type DecryptGroupSignalOpts = { 4 | group: string 5 | authorJid: string 6 | msg: Uint8Array 7 | } 8 | 9 | type ProcessSenderKeyDistributionMessageOpts = { 10 | item: proto.Message.ISenderKeyDistributionMessage 11 | authorJid: string 12 | } 13 | 14 | type DecryptSignalProtoOpts = { 15 | jid: string 16 | type: 'pkmsg' | 'msg' 17 | ciphertext: Uint8Array 18 | } 19 | 20 | type EncryptMessageOpts = { 21 | jid: string 22 | data: Uint8Array 23 | } 24 | 25 | type EncryptGroupMessageOpts = { 26 | group: string 27 | data: Uint8Array 28 | meId: string 29 | } 30 | 31 | type PreKey = { 32 | keyId: number 33 | publicKey: Uint8Array 34 | } 35 | 36 | type SignedPreKey = PreKey & { 37 | signature: Uint8Array 38 | } 39 | 40 | type E2ESession = { 41 | registrationId: number 42 | identityKey: Uint8Array 43 | signedPreKey: SignedPreKey 44 | preKey: PreKey 45 | } 46 | 47 | type E2ESessionOpts = { 48 | jid: string 49 | session: E2ESession 50 | } 51 | 52 | export type SignalRepository = { 53 | decryptGroupMessage(opts: DecryptGroupSignalOpts): Promise 54 | processSenderKeyDistributionMessage( 55 | opts: ProcessSenderKeyDistributionMessageOpts 56 | ): Promise 57 | decryptMessage(opts: DecryptSignalProtoOpts): Promise 58 | encryptMessage(opts: EncryptMessageOpts): Promise<{ 59 | type: 'pkmsg' | 'msg' 60 | ciphertext: Uint8Array 61 | }> 62 | encryptGroupMessage(opts: EncryptGroupMessageOpts): Promise<{ 63 | senderKeyDistributionMessage: Uint8Array 64 | ciphertext: Uint8Array 65 | }> 66 | injectE2ESession(opts: E2ESessionOpts): Promise 67 | jidToSignalProtocolAddress(jid: string): string 68 | } -------------------------------------------------------------------------------- /src/Types/Socket.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AxiosRequestConfig } from 'axios' 3 | import type { Agent } from 'https' 4 | import type { URL } from 'url' 5 | import { proto } from '../../WAProto' 6 | import { ILogger } from '../Utils/logger' 7 | import { AuthenticationState, SignalAuthState, TransactionCapabilityOptions } from './Auth' 8 | import { GroupMetadata } from './GroupMetadata' 9 | import { MediaConnInfo } from './Message' 10 | import { SignalRepository } from './Signal' 11 | 12 | export type WAVersion = [number, number, number] 13 | export type WABrowserDescription = [string, string, string] 14 | 15 | export type CacheStore = { 16 | /** get a cached key and change the stats */ 17 | get(key: string): T | undefined 18 | /** set a key in the cache */ 19 | set(key: string, value: T): void 20 | /** delete a key from the cache */ 21 | del(key: string): void 22 | /** flush all data */ 23 | flushAll(): void 24 | } 25 | 26 | export type PatchedMessageWithRecipientJID = proto.IMessage & {recipientJid?: string} 27 | 28 | export type SocketConfig = { 29 | /** the WS url to connect to WA */ 30 | waWebSocketUrl: string | URL 31 | /** Fails the connection if the socket times out in this interval */ 32 | connectTimeoutMs: number 33 | /** Default timeout for queries, undefined for no timeout */ 34 | defaultQueryTimeoutMs: number | undefined 35 | /** ping-pong interval for WS connection */ 36 | keepAliveIntervalMs: number 37 | /** should baileys use the mobile api instead of the multi device api 38 | * @deprecated This feature has been removed 39 | */ 40 | mobile?: boolean 41 | /** proxy agent */ 42 | agent?: Agent 43 | /** logger */ 44 | logger: ILogger 45 | /** version to connect with */ 46 | version: WAVersion 47 | /** override browser config */ 48 | browser: WABrowserDescription 49 | /** agent used for fetch requests -- uploading/downloading media */ 50 | fetchAgent?: Agent 51 | /** should the QR be printed in the terminal 52 | * @deprecated This feature has been removed 53 | */ 54 | printQRInTerminal?: boolean 55 | /** should events be emitted for actions done by this socket connection */ 56 | emitOwnEvents: boolean 57 | /** custom upload hosts to upload media to */ 58 | customUploadHosts: MediaConnInfo['hosts'] 59 | /** time to wait between sending new retry requests */ 60 | retryRequestDelayMs: number 61 | /** max retry count */ 62 | maxMsgRetryCount: number 63 | /** time to wait for the generation of the next QR in ms */ 64 | qrTimeout?: number 65 | /** provide an auth state object to maintain the auth state */ 66 | auth: AuthenticationState 67 | /** manage history processing with this control; by default will sync up everything */ 68 | shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => boolean 69 | /** transaction capability options for SignalKeyStore */ 70 | transactionOpts: TransactionCapabilityOptions 71 | /** marks the client as online whenever the socket successfully connects */ 72 | markOnlineOnConnect: boolean 73 | /** alphanumeric country code (USA -> US) for the number used */ 74 | countryCode: string 75 | /** provide a cache to store media, so does not have to be re-uploaded */ 76 | mediaCache?: CacheStore 77 | /** 78 | * map to store the retry counts for failed messages; 79 | * used to determine whether to retry a message or not */ 80 | msgRetryCounterCache?: CacheStore 81 | /** provide a cache to store a user's device list */ 82 | userDevicesCache?: CacheStore 83 | /** cache to store call offers */ 84 | callOfferCache?: CacheStore 85 | /** cache to track placeholder resends */ 86 | placeholderResendCache?: CacheStore 87 | /** width for link preview images */ 88 | linkPreviewImageThumbnailWidth: number 89 | /** Should Baileys ask the phone for full history, will be received async */ 90 | syncFullHistory: boolean 91 | /** Should baileys fire init queries automatically, default true */ 92 | fireInitQueries: boolean 93 | /** 94 | * generate a high quality link preview, 95 | * entails uploading the jpegThumbnail to WA 96 | * */ 97 | generateHighQualityLinkPreview: boolean 98 | 99 | /** 100 | * Returns if a jid should be ignored, 101 | * no event for that jid will be triggered. 102 | * Messages from that jid will also not be decrypted 103 | * */ 104 | shouldIgnoreJid: (jid: string) => boolean | undefined 105 | 106 | /** 107 | * Optionally patch the message before sending out 108 | * */ 109 | patchMessageBeforeSending: ( 110 | msg: proto.IMessage, 111 | recipientJids?: string[], 112 | ) => Promise | PatchedMessageWithRecipientJID[] | PatchedMessageWithRecipientJID 113 | 114 | /** verify app state MACs */ 115 | appStateMacVerification: { 116 | patch: boolean 117 | snapshot: boolean 118 | } 119 | 120 | /** options for axios */ 121 | options: AxiosRequestConfig<{}> 122 | /** 123 | * fetch a message from your store 124 | * implement this so that messages failed to send 125 | * (solves the "this message can take a while" issue) can be retried 126 | * */ 127 | getMessage: (key: proto.IMessageKey) => Promise 128 | 129 | /** cached group metadata, use to prevent redundant requests to WA & speed up msg sending */ 130 | cachedGroupMetadata: (jid: string) => Promise 131 | 132 | makeSignalRepository: (auth: SignalAuthState) => SignalRepository 133 | } 134 | -------------------------------------------------------------------------------- /src/Types/State.ts: -------------------------------------------------------------------------------- 1 | import { Contact } from './Contact' 2 | 3 | export type WAConnectionState = 'open' | 'connecting' | 'close' 4 | 5 | export type ConnectionState = { 6 | /** connection is now open, connecting or closed */ 7 | connection: WAConnectionState 8 | /** the error that caused the connection to close */ 9 | lastDisconnect?: { 10 | error: Error | undefined 11 | date: Date 12 | } 13 | /** is this a new login */ 14 | isNewLogin?: boolean 15 | /** the current QR code */ 16 | qr?: string 17 | /** has the device received all pending notifications while it was offline */ 18 | receivedPendingNotifications?: boolean 19 | /** legacy connection options */ 20 | legacy?: { 21 | phoneConnected: boolean 22 | user?: Contact 23 | } 24 | /** 25 | * if the client is shown as an active, online client. 26 | * If this is false, the primary phone and other devices will receive notifs 27 | * */ 28 | isOnline?: boolean 29 | } -------------------------------------------------------------------------------- /src/Types/USync.ts: -------------------------------------------------------------------------------- 1 | import { BinaryNode } from '../WABinary' 2 | import { USyncUser } from '../WAUSync' 3 | 4 | /** 5 | * Defines the interface for a USyncQuery protocol 6 | */ 7 | export interface USyncQueryProtocol { 8 | /** 9 | * The name of the protocol 10 | */ 11 | name: string 12 | /** 13 | * Defines what goes inside the query part of a USyncQuery 14 | */ 15 | getQueryElement: () => BinaryNode 16 | /** 17 | * Defines what goes inside the user part of a USyncQuery 18 | */ 19 | getUserElement: (user: USyncUser) => BinaryNode | null 20 | 21 | /** 22 | * Parse the result of the query 23 | * @param data Data from the result 24 | * @returns Whatever the protocol is supposed to return 25 | */ 26 | parser: (data: BinaryNode) => unknown 27 | } -------------------------------------------------------------------------------- /src/Types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Auth' 2 | export * from './GroupMetadata' 3 | export * from './Chat' 4 | export * from './Contact' 5 | export * from './State' 6 | export * from './Message' 7 | export * from './Socket' 8 | export * from './Events' 9 | export * from './Product' 10 | export * from './Call' 11 | export * from './Signal' 12 | 13 | import { AuthenticationState } from './Auth' 14 | import { SocketConfig } from './Socket' 15 | 16 | export type UserFacingSocketConfig = Partial & { auth: AuthenticationState } 17 | 18 | export type BrowsersMap = { 19 | ubuntu(browser: string): [string, string, string] 20 | macOS(browser: string): [string, string, string] 21 | baileys(browser: string): [string, string, string] 22 | windows(browser: string): [string, string, string] 23 | appropriate(browser: string): [string, string, string] 24 | } 25 | 26 | export enum DisconnectReason { 27 | connectionClosed = 428, 28 | connectionLost = 408, 29 | connectionReplaced = 440, 30 | timedOut = 408, 31 | loggedOut = 401, 32 | badSession = 500, 33 | restartRequired = 515, 34 | multideviceMismatch = 411, 35 | forbidden = 403, 36 | unavailableService = 503 37 | } 38 | 39 | export type WAInitResponse = { 40 | ref: string 41 | ttl: number 42 | status: 200 43 | } 44 | 45 | export type WABusinessHoursConfig = { 46 | day_of_week: string 47 | mode: string 48 | open_time?: number 49 | close_time?: number 50 | } 51 | 52 | export type WABusinessProfile = { 53 | description: string 54 | email: string | undefined 55 | business_hours: { 56 | timezone?: string 57 | config?: WABusinessHoursConfig[] 58 | business_config?: WABusinessHoursConfig[] 59 | } 60 | website: string[] 61 | category?: string 62 | wid?: string 63 | address?: string 64 | } 65 | 66 | export type CurveKeyPair = { private: Uint8Array, public: Uint8Array } 67 | -------------------------------------------------------------------------------- /src/Utils/auth-utils.ts: -------------------------------------------------------------------------------- 1 | import NodeCache from '@cacheable/node-cache' 2 | import { randomBytes } from 'crypto' 3 | import { DEFAULT_CACHE_TTLS } from '../Defaults' 4 | import type { AuthenticationCreds, CacheStore, SignalDataSet, SignalDataTypeMap, SignalKeyStore, SignalKeyStoreWithTransaction, TransactionCapabilityOptions } from '../Types' 5 | import { Curve, signedKeyPair } from './crypto' 6 | import { delay, generateRegistrationId } from './generics' 7 | import { ILogger } from './logger' 8 | 9 | /** 10 | * Adds caching capability to a SignalKeyStore 11 | * @param store the store to add caching to 12 | * @param logger to log trace events 13 | * @param _cache cache store to use 14 | */ 15 | export function makeCacheableSignalKeyStore( 16 | store: SignalKeyStore, 17 | logger?: ILogger, 18 | _cache?: CacheStore 19 | ): SignalKeyStore { 20 | const cache = _cache || new NodeCache({ 21 | stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, // 5 minutes 22 | useClones: false, 23 | deleteOnExpire: true, 24 | }) 25 | 26 | function getUniqueId(type: string, id: string) { 27 | return `${type}.${id}` 28 | } 29 | 30 | return { 31 | async get(type, ids) { 32 | const data: { [_: string]: SignalDataTypeMap[typeof type] } = { } 33 | const idsToFetch: string[] = [] 34 | for(const id of ids) { 35 | const item = cache.get(getUniqueId(type, id)) 36 | if(typeof item !== 'undefined') { 37 | data[id] = item 38 | } else { 39 | idsToFetch.push(id) 40 | } 41 | } 42 | 43 | if(idsToFetch.length) { 44 | logger?.trace({ items: idsToFetch.length }, 'loading from store') 45 | const fetched = await store.get(type, idsToFetch) 46 | for(const id of idsToFetch) { 47 | const item = fetched[id] 48 | if(item) { 49 | data[id] = item 50 | cache.set(getUniqueId(type, id), item) 51 | } 52 | } 53 | } 54 | 55 | return data 56 | }, 57 | async set(data) { 58 | let keys = 0 59 | for(const type in data) { 60 | for(const id in data[type]) { 61 | cache.set(getUniqueId(type, id), data[type][id]) 62 | keys += 1 63 | } 64 | } 65 | 66 | logger?.trace({ keys }, 'updated cache') 67 | 68 | await store.set(data) 69 | }, 70 | async clear() { 71 | cache.flushAll() 72 | await store.clear?.() 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * Adds DB like transaction capability (https://en.wikipedia.org/wiki/Database_transaction) to the SignalKeyStore, 79 | * this allows batch read & write operations & improves the performance of the lib 80 | * @param state the key store to apply this capability to 81 | * @param logger logger to log events 82 | * @returns SignalKeyStore with transaction capability 83 | */ 84 | export const addTransactionCapability = ( 85 | state: SignalKeyStore, 86 | logger: ILogger, 87 | { maxCommitRetries, delayBetweenTriesMs }: TransactionCapabilityOptions 88 | ): SignalKeyStoreWithTransaction => { 89 | // number of queries made to the DB during the transaction 90 | // only there for logging purposes 91 | let dbQueriesInTransaction = 0 92 | let transactionCache: SignalDataSet = { } 93 | let mutations: SignalDataSet = { } 94 | 95 | let transactionsInProgress = 0 96 | 97 | return { 98 | get: async(type, ids) => { 99 | if(isInTransaction()) { 100 | const dict = transactionCache[type] 101 | const idsRequiringFetch = dict 102 | ? ids.filter(item => typeof dict[item] === 'undefined') 103 | : ids 104 | // only fetch if there are any items to fetch 105 | if(idsRequiringFetch.length) { 106 | dbQueriesInTransaction += 1 107 | const result = await state.get(type, idsRequiringFetch) 108 | 109 | transactionCache[type] ||= {} 110 | Object.assign( 111 | transactionCache[type]!, 112 | result 113 | ) 114 | } 115 | 116 | return ids.reduce( 117 | (dict, id) => { 118 | const value = transactionCache[type]?.[id] 119 | if(value) { 120 | dict[id] = value 121 | } 122 | 123 | return dict 124 | }, { } 125 | ) 126 | } else { 127 | return state.get(type, ids) 128 | } 129 | }, 130 | set: data => { 131 | if(isInTransaction()) { 132 | logger.trace({ types: Object.keys(data) }, 'caching in transaction') 133 | for(const key in data) { 134 | transactionCache[key] = transactionCache[key] || { } 135 | Object.assign(transactionCache[key], data[key]) 136 | 137 | mutations[key] = mutations[key] || { } 138 | Object.assign(mutations[key], data[key]) 139 | } 140 | } else { 141 | return state.set(data) 142 | } 143 | }, 144 | isInTransaction, 145 | async transaction(work) { 146 | let result: Awaited> 147 | transactionsInProgress += 1 148 | if(transactionsInProgress === 1) { 149 | logger.trace('entering transaction') 150 | } 151 | 152 | try { 153 | result = await work() 154 | // commit if this is the outermost transaction 155 | if(transactionsInProgress === 1) { 156 | if(Object.keys(mutations).length) { 157 | logger.trace('committing transaction') 158 | // retry mechanism to ensure we've some recovery 159 | // in case a transaction fails in the first attempt 160 | let tries = maxCommitRetries 161 | while(tries) { 162 | tries -= 1 163 | //eslint-disable-next-line max-depth 164 | try { 165 | await state.set(mutations) 166 | logger.trace({ dbQueriesInTransaction }, 'committed transaction') 167 | break 168 | } catch(error) { 169 | logger.warn(`failed to commit ${Object.keys(mutations).length} mutations, tries left=${tries}`) 170 | await delay(delayBetweenTriesMs) 171 | } 172 | } 173 | } else { 174 | logger.trace('no mutations in transaction') 175 | } 176 | } 177 | } finally { 178 | transactionsInProgress -= 1 179 | if(transactionsInProgress === 0) { 180 | transactionCache = { } 181 | mutations = { } 182 | dbQueriesInTransaction = 0 183 | } 184 | } 185 | 186 | return result 187 | } 188 | } 189 | 190 | function isInTransaction() { 191 | return transactionsInProgress > 0 192 | } 193 | } 194 | 195 | export const initAuthCreds = (): AuthenticationCreds => { 196 | const identityKey = Curve.generateKeyPair() 197 | return { 198 | noiseKey: Curve.generateKeyPair(), 199 | pairingEphemeralKeyPair: Curve.generateKeyPair(), 200 | signedIdentityKey: identityKey, 201 | signedPreKey: signedKeyPair(identityKey, 1), 202 | registrationId: generateRegistrationId(), 203 | advSecretKey: randomBytes(32).toString('base64'), 204 | processedHistoryMessages: [], 205 | nextPreKeyId: 1, 206 | firstUnuploadedPreKeyId: 1, 207 | accountSyncCounter: 0, 208 | accountSettings: { 209 | unarchiveChats: false 210 | }, 211 | registered: false, 212 | pairingCode: undefined, 213 | lastPropHash: undefined, 214 | routingInfo: undefined, 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/Utils/baileys-event-stream.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events' 2 | import { createReadStream } from 'fs' 3 | import { writeFile } from 'fs/promises' 4 | import { createInterface } from 'readline' 5 | import type { BaileysEventEmitter } from '../Types' 6 | import { delay } from './generics' 7 | import { makeMutex } from './make-mutex' 8 | 9 | /** 10 | * Captures events from a baileys event emitter & stores them in a file 11 | * @param ev The event emitter to read events from 12 | * @param filename File to save to 13 | */ 14 | export const captureEventStream = (ev: BaileysEventEmitter, filename: string) => { 15 | const oldEmit = ev.emit 16 | // write mutex so data is appended in order 17 | const writeMutex = makeMutex() 18 | // monkey patch eventemitter to capture all events 19 | ev.emit = function(...args: any[]) { 20 | const content = JSON.stringify({ timestamp: Date.now(), event: args[0], data: args[1] }) + '\n' 21 | const result = oldEmit.apply(ev, args) 22 | 23 | writeMutex.mutex( 24 | async() => { 25 | await writeFile(filename, content, { flag: 'a' }) 26 | } 27 | ) 28 | 29 | return result 30 | } 31 | } 32 | 33 | /** 34 | * Read event file and emit events from there 35 | * @param filename filename containing event data 36 | * @param delayIntervalMs delay between each event emit 37 | */ 38 | export const readAndEmitEventStream = (filename: string, delayIntervalMs = 0) => { 39 | const ev = new EventEmitter() as BaileysEventEmitter 40 | 41 | const fireEvents = async() => { 42 | // from: https://stackoverflow.com/questions/6156501/read-a-file-one-line-at-a-time-in-node-js 43 | const fileStream = createReadStream(filename) 44 | 45 | const rl = createInterface({ 46 | input: fileStream, 47 | crlfDelay: Infinity 48 | }) 49 | // Note: we use the crlfDelay option to recognize all instances of CR LF 50 | // ('\r\n') in input.txt as a single line break. 51 | for await (const line of rl) { 52 | if(line) { 53 | const { event, data } = JSON.parse(line) 54 | ev.emit(event, data) 55 | delayIntervalMs && await delay(delayIntervalMs) 56 | } 57 | } 58 | 59 | fileStream.close() 60 | } 61 | 62 | return { 63 | ev, 64 | task: fireEvents() 65 | } 66 | } -------------------------------------------------------------------------------- /src/Utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto' 2 | import * as libsignal from 'libsignal' 3 | import { KEY_BUNDLE_TYPE } from '../Defaults' 4 | import { KeyPair } from '../Types' 5 | 6 | // insure browser & node compatibility 7 | const { subtle } = globalThis.crypto 8 | 9 | /** prefix version byte to the pub keys, required for some curve crypto functions */ 10 | export const generateSignalPubKey = (pubKey: Uint8Array | Buffer) => ( 11 | pubKey.length === 33 12 | ? pubKey 13 | : Buffer.concat([ KEY_BUNDLE_TYPE, pubKey ]) 14 | ) 15 | 16 | export const Curve = { 17 | generateKeyPair: (): KeyPair => { 18 | const { pubKey, privKey } = libsignal.curve.generateKeyPair() 19 | return { 20 | private: Buffer.from(privKey), 21 | // remove version byte 22 | public: Buffer.from((pubKey as Uint8Array).slice(1)) 23 | } 24 | }, 25 | sharedKey: (privateKey: Uint8Array, publicKey: Uint8Array) => { 26 | const shared = libsignal.curve.calculateAgreement(generateSignalPubKey(publicKey), privateKey) 27 | return Buffer.from(shared) 28 | }, 29 | sign: (privateKey: Uint8Array, buf: Uint8Array) => ( 30 | libsignal.curve.calculateSignature(privateKey, buf) 31 | ), 32 | verify: (pubKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => { 33 | try { 34 | libsignal.curve.verifySignature(generateSignalPubKey(pubKey), message, signature) 35 | return true 36 | } catch(error) { 37 | return false 38 | } 39 | } 40 | } 41 | 42 | export const signedKeyPair = (identityKeyPair: KeyPair, keyId: number) => { 43 | const preKey = Curve.generateKeyPair() 44 | const pubKey = generateSignalPubKey(preKey.public) 45 | 46 | const signature = Curve.sign(identityKeyPair.private, pubKey) 47 | 48 | return { keyPair: preKey, signature, keyId } 49 | } 50 | 51 | const GCM_TAG_LENGTH = 128 >> 3 52 | 53 | /** 54 | * encrypt AES 256 GCM; 55 | * where the tag tag is suffixed to the ciphertext 56 | * */ 57 | export function aesEncryptGCM(plaintext: Uint8Array, key: Uint8Array, iv: Uint8Array, additionalData: Uint8Array) { 58 | const cipher = createCipheriv('aes-256-gcm', key, iv) 59 | cipher.setAAD(additionalData) 60 | return Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]) 61 | } 62 | 63 | /** 64 | * decrypt AES 256 GCM; 65 | * where the auth tag is suffixed to the ciphertext 66 | * */ 67 | export function aesDecryptGCM(ciphertext: Uint8Array, key: Uint8Array, iv: Uint8Array, additionalData: Uint8Array) { 68 | const decipher = createDecipheriv('aes-256-gcm', key, iv) 69 | // decrypt additional adata 70 | const enc = ciphertext.slice(0, ciphertext.length - GCM_TAG_LENGTH) 71 | const tag = ciphertext.slice(ciphertext.length - GCM_TAG_LENGTH) 72 | // set additional data 73 | decipher.setAAD(additionalData) 74 | decipher.setAuthTag(tag) 75 | 76 | return Buffer.concat([ decipher.update(enc), decipher.final() ]) 77 | } 78 | 79 | export function aesEncryptCTR(plaintext: Uint8Array, key: Uint8Array, iv: Uint8Array) { 80 | const cipher = createCipheriv('aes-256-ctr', key, iv) 81 | return Buffer.concat([cipher.update(plaintext), cipher.final()]) 82 | } 83 | 84 | export function aesDecryptCTR(ciphertext: Uint8Array, key: Uint8Array, iv: Uint8Array) { 85 | const decipher = createDecipheriv('aes-256-ctr', key, iv) 86 | return Buffer.concat([decipher.update(ciphertext), decipher.final()]) 87 | } 88 | 89 | /** decrypt AES 256 CBC; where the IV is prefixed to the buffer */ 90 | export function aesDecrypt(buffer: Buffer, key: Buffer) { 91 | return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16)) 92 | } 93 | 94 | /** decrypt AES 256 CBC */ 95 | export function aesDecryptWithIV(buffer: Buffer, key: Buffer, IV: Buffer) { 96 | const aes = createDecipheriv('aes-256-cbc', key, IV) 97 | return Buffer.concat([aes.update(buffer), aes.final()]) 98 | } 99 | 100 | // encrypt AES 256 CBC; where a random IV is prefixed to the buffer 101 | export function aesEncrypt(buffer: Buffer | Uint8Array, key: Buffer) { 102 | const IV = randomBytes(16) 103 | const aes = createCipheriv('aes-256-cbc', key, IV) 104 | return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer 105 | } 106 | 107 | // encrypt AES 256 CBC with a given IV 108 | export function aesEncrypWithIV(buffer: Buffer, key: Buffer, IV: Buffer) { 109 | const aes = createCipheriv('aes-256-cbc', key, IV) 110 | return Buffer.concat([aes.update(buffer), aes.final()]) // prefix IV to the buffer 111 | } 112 | 113 | // sign HMAC using SHA 256 114 | export function hmacSign(buffer: Buffer | Uint8Array, key: Buffer | Uint8Array, variant: 'sha256' | 'sha512' = 'sha256') { 115 | return createHmac(variant, key).update(buffer).digest() 116 | } 117 | 118 | export function sha256(buffer: Buffer) { 119 | return createHash('sha256').update(buffer).digest() 120 | } 121 | 122 | export function md5(buffer: Buffer) { 123 | return createHash('md5').update(buffer).digest() 124 | } 125 | 126 | // HKDF key expansion 127 | export async function hkdf( 128 | buffer: Uint8Array | Buffer, 129 | expandedLength: number, 130 | info: { salt?: Buffer, info?: string } 131 | ): Promise { 132 | // Ensure we have a Uint8Array for the key material 133 | const inputKeyMaterial = buffer instanceof Uint8Array 134 | ? buffer 135 | : new Uint8Array(buffer) 136 | 137 | // Set default values if not provided 138 | const salt = info.salt ? new Uint8Array(info.salt) : new Uint8Array(0) 139 | const infoBytes = info.info 140 | ? new TextEncoder().encode(info.info) 141 | : new Uint8Array(0) 142 | 143 | // Import the input key material 144 | const importedKey = await subtle.importKey( 145 | 'raw', 146 | inputKeyMaterial, 147 | { name: 'HKDF' }, 148 | false, 149 | ['deriveBits'] 150 | ) 151 | 152 | // Derive bits using HKDF 153 | const derivedBits = await subtle.deriveBits( 154 | { 155 | name: 'HKDF', 156 | hash: 'SHA-256', 157 | salt: salt, 158 | info: infoBytes 159 | }, 160 | importedKey, 161 | expandedLength * 8 // Convert bytes to bits 162 | ) 163 | 164 | return Buffer.from(derivedBits) 165 | } 166 | 167 | 168 | export async function derivePairingCodeKey(pairingCode: string, salt: Buffer): Promise { 169 | // Convert inputs to formats Web Crypto API can work with 170 | const encoder = new TextEncoder() 171 | const pairingCodeBuffer = encoder.encode(pairingCode) 172 | const saltBuffer = salt instanceof Uint8Array ? salt : new Uint8Array(salt) 173 | 174 | // Import the pairing code as key material 175 | const keyMaterial = await subtle.importKey( 176 | 'raw', 177 | pairingCodeBuffer, 178 | { name: 'PBKDF2' }, 179 | false, 180 | ['deriveBits'] 181 | ) 182 | 183 | // Derive bits using PBKDF2 with the same parameters 184 | // 2 << 16 = 131,072 iterations 185 | const derivedBits = await subtle.deriveBits( 186 | { 187 | name: 'PBKDF2', 188 | salt: saltBuffer, 189 | iterations: 2 << 16, 190 | hash: 'SHA-256' 191 | }, 192 | keyMaterial, 193 | 32 * 8 // 32 bytes * 8 = 256 bits 194 | ) 195 | 196 | return Buffer.from(derivedBits) 197 | } 198 | -------------------------------------------------------------------------------- /src/Utils/decode-wa-message.ts: -------------------------------------------------------------------------------- 1 | import { Boom } from '@hapi/boom' 2 | import { proto } from '../../WAProto' 3 | import { SignalRepository, WAMessageKey } from '../Types' 4 | import { areJidsSameUser, BinaryNode, isJidBroadcast, isJidGroup, isJidMetaIa, isJidNewsletter, isJidStatusBroadcast, isJidUser, isLidUser } from '../WABinary' 5 | import { unpadRandomMax16 } from './generics' 6 | import { ILogger } from './logger' 7 | 8 | export const NO_MESSAGE_FOUND_ERROR_TEXT = 'Message absent from node' 9 | export const MISSING_KEYS_ERROR_TEXT = 'Key used already or never filled' 10 | 11 | export const NACK_REASONS = { 12 | ParsingError: 487, 13 | UnrecognizedStanza: 488, 14 | UnrecognizedStanzaClass: 489, 15 | UnrecognizedStanzaType: 490, 16 | InvalidProtobuf: 491, 17 | InvalidHostedCompanionStanza: 493, 18 | MissingMessageSecret: 495, 19 | SignalErrorOldCounter: 496, 20 | MessageDeletedOnPeer: 499, 21 | UnhandledError: 500, 22 | UnsupportedAdminRevoke: 550, 23 | UnsupportedLIDGroup: 551, 24 | DBOperationFailed: 552 25 | } 26 | 27 | type MessageType = 'chat' | 'peer_broadcast' | 'other_broadcast' | 'group' | 'direct_peer_status' | 'other_status' | 'newsletter' 28 | 29 | /** 30 | * Decode the received node as a message. 31 | * @note this will only parse the message, not decrypt it 32 | */ 33 | export function decodeMessageNode( 34 | stanza: BinaryNode, 35 | meId: string, 36 | meLid: string 37 | ) { 38 | let msgType: MessageType 39 | let chatId: string 40 | let author: string 41 | 42 | const msgId = stanza.attrs.id 43 | const from = stanza.attrs.from 44 | const participant: string | undefined = stanza.attrs.participant 45 | const recipient: string | undefined = stanza.attrs.recipient 46 | 47 | const isMe = (jid: string) => areJidsSameUser(jid, meId) 48 | const isMeLid = (jid: string) => areJidsSameUser(jid, meLid) 49 | 50 | if(isJidUser(from) || isLidUser(from)) { 51 | if(recipient && !isJidMetaIa(recipient)) { 52 | if(!isMe(from) && !isMeLid(from)) { 53 | throw new Boom('receipient present, but msg not from me', { data: stanza }) 54 | } 55 | 56 | chatId = recipient 57 | } else { 58 | chatId = from 59 | } 60 | 61 | msgType = 'chat' 62 | author = from 63 | } else if(isJidGroup(from)) { 64 | if(!participant) { 65 | throw new Boom('No participant in group message') 66 | } 67 | 68 | msgType = 'group' 69 | author = participant 70 | chatId = from 71 | } else if(isJidBroadcast(from)) { 72 | if(!participant) { 73 | throw new Boom('No participant in group message') 74 | } 75 | 76 | const isParticipantMe = isMe(participant) 77 | if(isJidStatusBroadcast(from)) { 78 | msgType = isParticipantMe ? 'direct_peer_status' : 'other_status' 79 | } else { 80 | msgType = isParticipantMe ? 'peer_broadcast' : 'other_broadcast' 81 | } 82 | 83 | chatId = from 84 | author = participant 85 | } else if(isJidNewsletter(from)) { 86 | msgType = 'newsletter' 87 | chatId = from 88 | author = from 89 | } else { 90 | throw new Boom('Unknown message type', { data: stanza }) 91 | } 92 | 93 | const fromMe = (isLidUser(from) ? isMeLid : isMe)(stanza.attrs.participant || stanza.attrs.from) 94 | const pushname = stanza?.attrs?.notify 95 | 96 | const key: WAMessageKey = { 97 | remoteJid: chatId, 98 | fromMe, 99 | id: msgId, 100 | participant 101 | } 102 | 103 | const fullMessage: proto.IWebMessageInfo = { 104 | key, 105 | messageTimestamp: +stanza.attrs.t, 106 | pushName: pushname, 107 | broadcast: isJidBroadcast(from) 108 | } 109 | 110 | if(key.fromMe) { 111 | fullMessage.status = proto.WebMessageInfo.Status.SERVER_ACK 112 | } 113 | 114 | return { 115 | fullMessage, 116 | author, 117 | sender: msgType === 'chat' ? author : chatId 118 | } 119 | } 120 | 121 | export const decryptMessageNode = ( 122 | stanza: BinaryNode, 123 | meId: string, 124 | meLid: string, 125 | repository: SignalRepository, 126 | logger: ILogger 127 | ) => { 128 | const { fullMessage, author, sender } = decodeMessageNode(stanza, meId, meLid) 129 | return { 130 | fullMessage, 131 | category: stanza.attrs.category, 132 | author, 133 | async decrypt() { 134 | let decryptables = 0 135 | if(Array.isArray(stanza.content)) { 136 | for(const { tag, attrs, content } of stanza.content) { 137 | if(tag === 'verified_name' && content instanceof Uint8Array) { 138 | const cert = proto.VerifiedNameCertificate.decode(content) 139 | const details = proto.VerifiedNameCertificate.Details.decode(cert.details!) 140 | fullMessage.verifiedBizName = details.verifiedName 141 | } 142 | 143 | if(tag !== 'enc' && tag !== 'plaintext') { 144 | continue 145 | } 146 | 147 | if(!(content instanceof Uint8Array)) { 148 | continue 149 | } 150 | 151 | decryptables += 1 152 | 153 | let msgBuffer: Uint8Array 154 | 155 | try { 156 | const e2eType = tag === 'plaintext' ? 'plaintext' : attrs.type 157 | switch (e2eType) { 158 | case 'skmsg': 159 | msgBuffer = await repository.decryptGroupMessage({ 160 | group: sender, 161 | authorJid: author, 162 | msg: content 163 | }) 164 | break 165 | case 'pkmsg': 166 | case 'msg': 167 | const user = isJidUser(sender) ? sender : author 168 | msgBuffer = await repository.decryptMessage({ 169 | jid: user, 170 | type: e2eType, 171 | ciphertext: content 172 | }) 173 | break 174 | case 'plaintext': 175 | msgBuffer = content 176 | break 177 | default: 178 | throw new Error(`Unknown e2e type: ${e2eType}`) 179 | } 180 | 181 | let msg: proto.IMessage = proto.Message.decode(e2eType !== 'plaintext' ? unpadRandomMax16(msgBuffer) : msgBuffer) 182 | msg = msg.deviceSentMessage?.message || msg 183 | if(msg.senderKeyDistributionMessage) { 184 | //eslint-disable-next-line max-depth 185 | try { 186 | await repository.processSenderKeyDistributionMessage({ 187 | authorJid: author, 188 | item: msg.senderKeyDistributionMessage 189 | }) 190 | } catch(err) { 191 | logger.error({ key: fullMessage.key, err }, 'failed to decrypt message') 192 | } 193 | } 194 | 195 | if(fullMessage.message) { 196 | Object.assign(fullMessage.message, msg) 197 | } else { 198 | fullMessage.message = msg 199 | } 200 | } catch(err) { 201 | logger.error( 202 | { key: fullMessage.key, err }, 203 | 'failed to decrypt message' 204 | ) 205 | fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT 206 | fullMessage.messageStubParameters = [err.message] 207 | } 208 | } 209 | } 210 | 211 | // if nothing was found to decrypt 212 | if(!decryptables) { 213 | fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT 214 | fullMessage.messageStubParameters = [NO_MESSAGE_FOUND_ERROR_TEXT] 215 | } 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/Utils/history.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios' 2 | import { promisify } from 'util' 3 | import { inflate } from 'zlib' 4 | import { proto } from '../../WAProto' 5 | import { Chat, Contact, WAMessageStubType } from '../Types' 6 | import { isJidUser } from '../WABinary' 7 | import { toNumber } from './generics' 8 | import { normalizeMessageContent } from './messages' 9 | import { downloadContentFromMessage } from './messages-media' 10 | 11 | const inflatePromise = promisify(inflate) 12 | 13 | export const downloadHistory = async( 14 | msg: proto.Message.IHistorySyncNotification, 15 | options: AxiosRequestConfig<{}> 16 | ) => { 17 | const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options }) 18 | const bufferArray: Buffer[] = [] 19 | for await (const chunk of stream) { 20 | bufferArray.push(chunk) 21 | } 22 | 23 | let buffer = Buffer.concat(bufferArray) 24 | 25 | // decompress buffer 26 | buffer = await inflatePromise(buffer) 27 | 28 | const syncData = proto.HistorySync.decode(buffer) 29 | return syncData 30 | } 31 | 32 | export const processHistoryMessage = (item: proto.IHistorySync) => { 33 | const messages: proto.IWebMessageInfo[] = [] 34 | const contacts: Contact[] = [] 35 | const chats: Chat[] = [] 36 | 37 | switch (item.syncType) { 38 | case proto.HistorySync.HistorySyncType.INITIAL_BOOTSTRAP: 39 | case proto.HistorySync.HistorySyncType.RECENT: 40 | case proto.HistorySync.HistorySyncType.FULL: 41 | case proto.HistorySync.HistorySyncType.ON_DEMAND: 42 | for(const chat of item.conversations! as Chat[]) { 43 | contacts.push({ id: chat.id, name: chat.name || undefined }) 44 | 45 | const msgs = chat.messages || [] 46 | delete chat.messages 47 | delete chat.archived 48 | delete chat.muteEndTime 49 | delete chat.pinned 50 | 51 | for(const item of msgs) { 52 | const message = item.message! 53 | messages.push(message) 54 | 55 | if(!chat.messages?.length) { 56 | // keep only the most recent message in the chat array 57 | chat.messages = [{ message }] 58 | } 59 | 60 | if(!message.key.fromMe && !chat.lastMessageRecvTimestamp) { 61 | chat.lastMessageRecvTimestamp = toNumber(message.messageTimestamp) 62 | } 63 | 64 | if( 65 | (message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_BSP 66 | || message.messageStubType === WAMessageStubType.BIZ_PRIVACY_MODE_TO_FB 67 | ) 68 | && message.messageStubParameters?.[0] 69 | ) { 70 | contacts.push({ 71 | id: message.key.participant || message.key.remoteJid!, 72 | verifiedName: message.messageStubParameters?.[0], 73 | }) 74 | } 75 | } 76 | 77 | if(isJidUser(chat.id) && chat.readOnly && chat.archived) { 78 | delete chat.readOnly 79 | } 80 | 81 | chats.push({ ...chat }) 82 | } 83 | 84 | break 85 | case proto.HistorySync.HistorySyncType.PUSH_NAME: 86 | for(const c of item.pushnames!) { 87 | contacts.push({ id: c.id!, notify: c.pushname! }) 88 | } 89 | 90 | break 91 | } 92 | 93 | return { 94 | chats, 95 | contacts, 96 | messages, 97 | syncType: item.syncType, 98 | progress: item.progress 99 | } 100 | } 101 | 102 | export const downloadAndProcessHistorySyncNotification = async( 103 | msg: proto.Message.IHistorySyncNotification, 104 | options: AxiosRequestConfig<{}> 105 | ) => { 106 | const historyMsg = await downloadHistory(msg, options) 107 | return processHistoryMessage(historyMsg) 108 | } 109 | 110 | export const getHistoryMsg = (message: proto.IMessage) => { 111 | const normalizedContent = !!message ? normalizeMessageContent(message) : undefined 112 | const anyHistoryMsg = normalizedContent?.protocolMessage?.historySyncNotification 113 | 114 | return anyHistoryMsg 115 | } -------------------------------------------------------------------------------- /src/Utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generics' 2 | export * from './decode-wa-message' 3 | export * from './messages' 4 | export * from './messages-media' 5 | export * from './validate-connection' 6 | export * from './crypto' 7 | export * from './signal' 8 | export * from './noise-handler' 9 | export * from './history' 10 | export * from './chat-utils' 11 | export * from './lt-hash' 12 | export * from './auth-utils' 13 | export * from './baileys-event-stream' 14 | export * from './use-multi-file-auth-state' 15 | export * from './link-preview' 16 | export * from './event-buffer' 17 | export * from './process-message' 18 | -------------------------------------------------------------------------------- /src/Utils/link-preview.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from 'axios' 2 | import { WAMediaUploadFunction, WAUrlInfo } from '../Types' 3 | import { ILogger } from './logger' 4 | import { prepareWAMessageMedia } from './messages' 5 | import { extractImageThumb, getHttpStream } from './messages-media' 6 | 7 | const THUMBNAIL_WIDTH_PX = 192 8 | 9 | /** Fetches an image and generates a thumbnail for it */ 10 | const getCompressedJpegThumbnail = async( 11 | url: string, 12 | { thumbnailWidth, fetchOpts }: URLGenerationOptions 13 | ) => { 14 | const stream = await getHttpStream(url, fetchOpts) 15 | const result = await extractImageThumb(stream, thumbnailWidth) 16 | return result 17 | } 18 | 19 | export type URLGenerationOptions = { 20 | thumbnailWidth: number 21 | fetchOpts: { 22 | /** Timeout in ms */ 23 | timeout: number 24 | proxyUrl?: string 25 | headers?: AxiosRequestConfig<{}>['headers'] 26 | } 27 | uploadImage?: WAMediaUploadFunction 28 | logger?: ILogger 29 | } 30 | 31 | /** 32 | * Given a piece of text, checks for any URL present, generates link preview for the same and returns it 33 | * Return undefined if the fetch failed or no URL was found 34 | * @param text first matched URL in text 35 | * @returns the URL info required to generate link preview 36 | */ 37 | export const getUrlInfo = async( 38 | text: string, 39 | opts: URLGenerationOptions = { 40 | thumbnailWidth: THUMBNAIL_WIDTH_PX, 41 | fetchOpts: { timeout: 3000 } 42 | }, 43 | ): Promise => { 44 | try { 45 | // retries 46 | const retries = 0 47 | const maxRetry = 5 48 | 49 | const { getLinkPreview } = await import('link-preview-js') 50 | let previewLink = text 51 | if(!text.startsWith('https://') && !text.startsWith('http://')) { 52 | previewLink = 'https://' + previewLink 53 | } 54 | 55 | const info = await getLinkPreview(previewLink, { 56 | ...opts.fetchOpts, 57 | followRedirects: 'follow', 58 | handleRedirects: (baseURL: string, forwardedURL: string) => { 59 | const urlObj = new URL(baseURL) 60 | const forwardedURLObj = new URL(forwardedURL) 61 | if(retries >= maxRetry) { 62 | return false 63 | } 64 | 65 | if( 66 | forwardedURLObj.hostname === urlObj.hostname 67 | || forwardedURLObj.hostname === 'www.' + urlObj.hostname 68 | || 'www.' + forwardedURLObj.hostname === urlObj.hostname 69 | ) { 70 | retries + 1 71 | return true 72 | } else { 73 | return false 74 | } 75 | }, 76 | headers: opts.fetchOpts as {} 77 | }) 78 | if(info && 'title' in info && info.title) { 79 | const [image] = info.images 80 | 81 | const urlInfo: WAUrlInfo = { 82 | 'canonical-url': info.url, 83 | 'matched-text': text, 84 | title: info.title, 85 | description: info.description, 86 | originalThumbnailUrl: image 87 | } 88 | 89 | if(opts.uploadImage) { 90 | const { imageMessage } = await prepareWAMessageMedia( 91 | { image: { url: image } }, 92 | { 93 | upload: opts.uploadImage, 94 | mediaTypeOverride: 'thumbnail-link', 95 | options: opts.fetchOpts 96 | } 97 | ) 98 | urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail 99 | ? Buffer.from(imageMessage.jpegThumbnail) 100 | : undefined 101 | urlInfo.highQualityThumbnail = imageMessage || undefined 102 | } else { 103 | try { 104 | urlInfo.jpegThumbnail = image 105 | ? (await getCompressedJpegThumbnail(image, opts)).buffer 106 | : undefined 107 | } catch(error) { 108 | opts.logger?.debug( 109 | { err: error.stack, url: previewLink }, 110 | 'error in generating thumbnail' 111 | ) 112 | } 113 | } 114 | 115 | return urlInfo 116 | } 117 | } catch(error) { 118 | if(!error.message.includes('receive a valid')) { 119 | throw error 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /src/Utils/logger.ts: -------------------------------------------------------------------------------- 1 | import P from 'pino' 2 | 3 | export interface ILogger { 4 | level: string 5 | child(obj: Record): ILogger 6 | trace(obj: unknown, msg?: string) 7 | debug(obj: unknown, msg?: string) 8 | info(obj: unknown, msg?: string) 9 | warn(obj: unknown, msg?: string) 10 | error(obj: unknown, msg?: string) 11 | } 12 | 13 | export default P({ timestamp: () => `,"time":"${new Date().toJSON()}"` }) 14 | -------------------------------------------------------------------------------- /src/Utils/lt-hash.ts: -------------------------------------------------------------------------------- 1 | import { hkdf } from './crypto' 2 | 3 | /** 4 | * LT Hash is a summation based hash algorithm that maintains the integrity of a piece of data 5 | * over a series of mutations. You can add/remove mutations and it'll return a hash equal to 6 | * if the same series of mutations was made sequentially. 7 | */ 8 | 9 | const o = 128 10 | 11 | class d { 12 | 13 | salt: string 14 | 15 | constructor(e: string) { 16 | this.salt = e 17 | } 18 | add(e, t) { 19 | var r = this 20 | for(const item of t) { 21 | e = r._addSingle(e, item) 22 | } 23 | 24 | return e 25 | } 26 | subtract(e, t) { 27 | var r = this 28 | for(const item of t) { 29 | e = r._subtractSingle(e, item) 30 | } 31 | 32 | return e 33 | } 34 | subtractThenAdd(e, t, r) { 35 | var n = this 36 | return n.add(n.subtract(e, r), t) 37 | } 38 | async _addSingle(e, t) { 39 | var r = this 40 | const n = new Uint8Array(await hkdf(Buffer.from(t), o, { info: r.salt })).buffer 41 | return r.performPointwiseWithOverflow(await e, n, ((e, t) => e + t)) 42 | } 43 | async _subtractSingle(e, t) { 44 | var r = this 45 | 46 | const n = new Uint8Array(await hkdf(Buffer.from(t), o, { info: r.salt })).buffer 47 | return r.performPointwiseWithOverflow(await e, n, ((e, t) => e - t)) 48 | } 49 | performPointwiseWithOverflow(e, t, r) { 50 | const n = new DataView(e) 51 | , i = new DataView(t) 52 | , a = new ArrayBuffer(n.byteLength) 53 | , s = new DataView(a) 54 | for(let e = 0; e < n.byteLength; e += 2) { 55 | s.setUint16(e, r(n.getUint16(e, !0), i.getUint16(e, !0)), !0) 56 | } 57 | 58 | return a 59 | } 60 | } 61 | export const LT_HASH_ANTI_TAMPERING = new d('WhatsApp Patch Integrity') 62 | -------------------------------------------------------------------------------- /src/Utils/make-mutex.ts: -------------------------------------------------------------------------------- 1 | export const makeMutex = () => { 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | let task = Promise.resolve() as Promise 4 | 5 | let taskTimeout: NodeJS.Timeout | undefined 6 | 7 | return { 8 | mutex(code: () => Promise | T): Promise { 9 | task = (async() => { 10 | // wait for the previous task to complete 11 | // if there is an error, we swallow so as to not block the queue 12 | try { 13 | await task 14 | } catch{ } 15 | 16 | try { 17 | // execute the current task 18 | const result = await code() 19 | return result 20 | } finally { 21 | clearTimeout(taskTimeout) 22 | } 23 | })() 24 | // we replace the existing task, appending the new piece of execution to it 25 | // so the next task will have to wait for this one to finish 26 | return task 27 | }, 28 | } 29 | } 30 | 31 | export type Mutex = ReturnType 32 | 33 | export const makeKeyedMutex = () => { 34 | const map: { [id: string]: Mutex } = {} 35 | 36 | return { 37 | mutex(key: string, task: () => Promise | T): Promise { 38 | if(!map[key]) { 39 | map[key] = makeMutex() 40 | } 41 | 42 | return map[key].mutex(task) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Utils/noise-handler.ts: -------------------------------------------------------------------------------- 1 | import { Boom } from '@hapi/boom' 2 | import { proto } from '../../WAProto' 3 | import { NOISE_MODE, WA_CERT_DETAILS } from '../Defaults' 4 | import { KeyPair } from '../Types' 5 | import { BinaryNode, decodeBinaryNode } from '../WABinary' 6 | import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto' 7 | import { ILogger } from './logger' 8 | 9 | const generateIV = (counter: number) => { 10 | const iv = new ArrayBuffer(12) 11 | new DataView(iv).setUint32(8, counter) 12 | 13 | return new Uint8Array(iv) 14 | } 15 | 16 | export const makeNoiseHandler = ({ 17 | keyPair: { private: privateKey, public: publicKey }, 18 | NOISE_HEADER, 19 | logger, 20 | routingInfo 21 | }: { 22 | keyPair: KeyPair 23 | NOISE_HEADER: Uint8Array 24 | logger: ILogger 25 | routingInfo?: Buffer | undefined 26 | }) => { 27 | logger = logger.child({ class: 'ns' }) 28 | 29 | const authenticate = (data: Uint8Array) => { 30 | if(!isFinished) { 31 | hash = sha256(Buffer.concat([hash, data])) 32 | } 33 | } 34 | 35 | const encrypt = (plaintext: Uint8Array) => { 36 | const result = aesEncryptGCM(plaintext, encKey, generateIV(writeCounter), hash) 37 | 38 | writeCounter += 1 39 | 40 | authenticate(result) 41 | return result 42 | } 43 | 44 | const decrypt = (ciphertext: Uint8Array) => { 45 | // before the handshake is finished, we use the same counter 46 | // after handshake, the counters are different 47 | const iv = generateIV(isFinished ? readCounter : writeCounter) 48 | const result = aesDecryptGCM(ciphertext, decKey, iv, hash) 49 | 50 | if(isFinished) { 51 | readCounter += 1 52 | } else { 53 | writeCounter += 1 54 | } 55 | 56 | authenticate(ciphertext) 57 | return result 58 | } 59 | 60 | const localHKDF = async(data: Uint8Array) => { 61 | const key = await hkdf(Buffer.from(data), 64, { salt, info: '' }) 62 | return [key.slice(0, 32), key.slice(32)] 63 | } 64 | 65 | const mixIntoKey = async(data: Uint8Array) => { 66 | const [write, read] = await localHKDF(data) 67 | salt = write 68 | encKey = read 69 | decKey = read 70 | readCounter = 0 71 | writeCounter = 0 72 | } 73 | 74 | const finishInit = async() => { 75 | const [write, read] = await localHKDF(new Uint8Array(0)) 76 | encKey = write 77 | decKey = read 78 | hash = Buffer.from([]) 79 | readCounter = 0 80 | writeCounter = 0 81 | isFinished = true 82 | } 83 | 84 | const data = Buffer.from(NOISE_MODE) 85 | let hash = data.byteLength === 32 ? data : sha256(data) 86 | let salt = hash 87 | let encKey = hash 88 | let decKey = hash 89 | let readCounter = 0 90 | let writeCounter = 0 91 | let isFinished = false 92 | let sentIntro = false 93 | 94 | let inBytes = Buffer.alloc(0) 95 | 96 | authenticate(NOISE_HEADER) 97 | authenticate(publicKey) 98 | 99 | return { 100 | encrypt, 101 | decrypt, 102 | authenticate, 103 | mixIntoKey, 104 | finishInit, 105 | processHandshake: async({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => { 106 | authenticate(serverHello!.ephemeral!) 107 | await mixIntoKey(Curve.sharedKey(privateKey, serverHello!.ephemeral!)) 108 | 109 | const decStaticContent = decrypt(serverHello!.static!) 110 | await mixIntoKey(Curve.sharedKey(privateKey, decStaticContent)) 111 | 112 | const certDecoded = decrypt(serverHello!.payload!) 113 | 114 | const { intermediate: certIntermediate } = proto.CertChain.decode(certDecoded) 115 | 116 | const { issuerSerial } = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate!.details!) 117 | 118 | if(issuerSerial !== WA_CERT_DETAILS.SERIAL) { 119 | throw new Boom('certification match failed', { statusCode: 400 }) 120 | } 121 | 122 | const keyEnc = encrypt(noiseKey.public) 123 | await mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!)) 124 | 125 | return keyEnc 126 | }, 127 | encodeFrame: (data: Buffer | Uint8Array) => { 128 | if(isFinished) { 129 | data = encrypt(data) 130 | } 131 | 132 | let header: Buffer 133 | 134 | if(routingInfo) { 135 | header = Buffer.alloc(7) 136 | header.write('ED', 0, 'utf8') 137 | header.writeUint8(0, 2) 138 | header.writeUint8(1, 3) 139 | header.writeUint8(routingInfo.byteLength >> 16, 4) 140 | header.writeUint16BE(routingInfo.byteLength & 65535, 5) 141 | header = Buffer.concat([header, routingInfo, NOISE_HEADER]) 142 | } else { 143 | header = Buffer.from(NOISE_HEADER) 144 | } 145 | 146 | const introSize = sentIntro ? 0 : header.length 147 | const frame = Buffer.alloc(introSize + 3 + data.byteLength) 148 | 149 | if(!sentIntro) { 150 | frame.set(header) 151 | sentIntro = true 152 | } 153 | 154 | frame.writeUInt8(data.byteLength >> 16, introSize) 155 | frame.writeUInt16BE(65535 & data.byteLength, introSize + 1) 156 | frame.set(data, introSize + 3) 157 | 158 | return frame 159 | }, 160 | decodeFrame: async(newData: Buffer | Uint8Array, onFrame: (buff: Uint8Array | BinaryNode) => void) => { 161 | // the binary protocol uses its own framing mechanism 162 | // on top of the WS frames 163 | // so we get this data and separate out the frames 164 | const getBytesSize = () => { 165 | if(inBytes.length >= 3) { 166 | return (inBytes.readUInt8() << 16) | inBytes.readUInt16BE(1) 167 | } 168 | } 169 | 170 | inBytes = Buffer.concat([ inBytes, newData ]) 171 | 172 | logger.trace(`recv ${newData.length} bytes, total recv ${inBytes.length} bytes`) 173 | 174 | let size = getBytesSize() 175 | while(size && inBytes.length >= size + 3) { 176 | let frame: Uint8Array | BinaryNode = inBytes.slice(3, size + 3) 177 | inBytes = inBytes.slice(size + 3) 178 | 179 | if(isFinished) { 180 | const result = decrypt(frame) 181 | frame = await decodeBinaryNode(result) 182 | } 183 | 184 | logger.trace({ msg: (frame as BinaryNode)?.attrs?.id }, 'recv frame') 185 | 186 | onFrame(frame) 187 | size = getBytesSize() 188 | } 189 | } 190 | } 191 | } -------------------------------------------------------------------------------- /src/Utils/signal.ts: -------------------------------------------------------------------------------- 1 | import { chunk } from 'lodash' 2 | import { KEY_BUNDLE_TYPE } from '../Defaults' 3 | import { SignalRepository } from '../Types' 4 | import { AuthenticationCreds, AuthenticationState, KeyPair, SignalIdentity, SignalKeyStore, SignedKeyPair } from '../Types/Auth' 5 | import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, jidDecode, JidWithDevice, S_WHATSAPP_NET } from '../WABinary' 6 | import { DeviceListData, ParsedDeviceInfo, USyncQueryResultList } from '../WAUSync' 7 | import { Curve, generateSignalPubKey } from './crypto' 8 | import { encodeBigEndian } from './generics' 9 | 10 | export const createSignalIdentity = ( 11 | wid: string, 12 | accountSignatureKey: Uint8Array 13 | ): SignalIdentity => { 14 | return { 15 | identifier: { name: wid, deviceId: 0 }, 16 | identifierKey: generateSignalPubKey(accountSignatureKey) 17 | } 18 | } 19 | 20 | export const getPreKeys = async({ get }: SignalKeyStore, min: number, limit: number) => { 21 | const idList: string[] = [] 22 | for(let id = min; id < limit;id++) { 23 | idList.push(id.toString()) 24 | } 25 | 26 | return get('pre-key', idList) 27 | } 28 | 29 | export const generateOrGetPreKeys = (creds: AuthenticationCreds, range: number) => { 30 | const avaliable = creds.nextPreKeyId - creds.firstUnuploadedPreKeyId 31 | const remaining = range - avaliable 32 | const lastPreKeyId = creds.nextPreKeyId + remaining - 1 33 | const newPreKeys: { [id: number]: KeyPair } = { } 34 | if(remaining > 0) { 35 | for(let i = creds.nextPreKeyId;i <= lastPreKeyId;i++) { 36 | newPreKeys[i] = Curve.generateKeyPair() 37 | } 38 | } 39 | 40 | return { 41 | newPreKeys, 42 | lastPreKeyId, 43 | preKeysRange: [creds.firstUnuploadedPreKeyId, range] as const, 44 | } 45 | } 46 | 47 | export const xmppSignedPreKey = (key: SignedKeyPair): BinaryNode => ( 48 | { 49 | tag: 'skey', 50 | attrs: { }, 51 | content: [ 52 | { tag: 'id', attrs: { }, content: encodeBigEndian(key.keyId, 3) }, 53 | { tag: 'value', attrs: { }, content: key.keyPair.public }, 54 | { tag: 'signature', attrs: { }, content: key.signature } 55 | ] 56 | } 57 | ) 58 | 59 | export const xmppPreKey = (pair: KeyPair, id: number): BinaryNode => ( 60 | { 61 | tag: 'key', 62 | attrs: { }, 63 | content: [ 64 | { tag: 'id', attrs: { }, content: encodeBigEndian(id, 3) }, 65 | { tag: 'value', attrs: { }, content: pair.public } 66 | ] 67 | } 68 | ) 69 | 70 | export const parseAndInjectE2ESessions = async( 71 | node: BinaryNode, 72 | repository: SignalRepository 73 | ) => { 74 | const extractKey = (key: BinaryNode) => ( 75 | key ? ({ 76 | keyId: getBinaryNodeChildUInt(key, 'id', 3)!, 77 | publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')!), 78 | signature: getBinaryNodeChildBuffer(key, 'signature')!, 79 | }) : undefined 80 | ) 81 | const nodes = getBinaryNodeChildren(getBinaryNodeChild(node, 'list'), 'user') 82 | for(const node of nodes) { 83 | assertNodeErrorFree(node) 84 | } 85 | 86 | // Most of the work in repository.injectE2ESession is CPU intensive, not IO 87 | // So Promise.all doesn't really help here, 88 | // but blocks even loop if we're using it inside keys.transaction, and it makes it "sync" actually 89 | // This way we chunk it in smaller parts and between those parts we can yield to the event loop 90 | // It's rare case when you need to E2E sessions for so many users, but it's possible 91 | const chunkSize = 100 92 | const chunks = chunk(nodes, chunkSize) 93 | for(const nodesChunk of chunks) { 94 | await Promise.all( 95 | nodesChunk.map( 96 | async node => { 97 | const signedKey = getBinaryNodeChild(node, 'skey')! 98 | const key = getBinaryNodeChild(node, 'key')! 99 | const identity = getBinaryNodeChildBuffer(node, 'identity')! 100 | const jid = node.attrs.jid 101 | const registrationId = getBinaryNodeChildUInt(node, 'registration', 4) 102 | 103 | await repository.injectE2ESession({ 104 | jid, 105 | session: { 106 | registrationId: registrationId!, 107 | identityKey: generateSignalPubKey(identity), 108 | signedPreKey: extractKey(signedKey)!, 109 | preKey: extractKey(key)! 110 | } 111 | }) 112 | } 113 | ) 114 | ) 115 | } 116 | } 117 | 118 | export const extractDeviceJids = (result: USyncQueryResultList[], myJid: string, excludeZeroDevices: boolean) => { 119 | const { user: myUser, device: myDevice } = jidDecode(myJid)! 120 | 121 | const extracted: JidWithDevice[] = [] 122 | 123 | 124 | for(const userResult of result) { 125 | const { devices, id } = userResult as { devices: ParsedDeviceInfo, id: string } 126 | const { user } = jidDecode(id)! 127 | const deviceList = devices?.deviceList as DeviceListData[] 128 | if(Array.isArray(deviceList)) { 129 | for(const { id: device, keyIndex } of deviceList) { 130 | if( 131 | (!excludeZeroDevices || device !== 0) && // if zero devices are not-excluded, or device is non zero 132 | (myUser !== user || myDevice !== device) && // either different user or if me user, not this device 133 | (device === 0 || !!keyIndex) // ensure that "key-index" is specified for "non-zero" devices, produces a bad req otherwise 134 | ) { 135 | extracted.push({ user, device }) 136 | } 137 | } 138 | } 139 | } 140 | 141 | return extracted 142 | } 143 | 144 | /** 145 | * get the next N keys for upload or processing 146 | * @param count number of pre-keys to get or generate 147 | */ 148 | export const getNextPreKeys = async({ creds, keys }: AuthenticationState, count: number) => { 149 | const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(creds, count) 150 | 151 | const update: Partial = { 152 | nextPreKeyId: Math.max(lastPreKeyId + 1, creds.nextPreKeyId), 153 | firstUnuploadedPreKeyId: Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId + 1) 154 | } 155 | 156 | await keys.set({ 'pre-key': newPreKeys }) 157 | 158 | const preKeys = await getPreKeys(keys, preKeysRange[0], preKeysRange[0] + preKeysRange[1]) 159 | 160 | return { update, preKeys } 161 | } 162 | 163 | export const getNextPreKeysNode = async(state: AuthenticationState, count: number) => { 164 | const { creds } = state 165 | const { update, preKeys } = await getNextPreKeys(state, count) 166 | 167 | const node: BinaryNode = { 168 | tag: 'iq', 169 | attrs: { 170 | xmlns: 'encrypt', 171 | type: 'set', 172 | to: S_WHATSAPP_NET, 173 | }, 174 | content: [ 175 | { tag: 'registration', attrs: { }, content: encodeBigEndian(creds.registrationId) }, 176 | { tag: 'type', attrs: { }, content: KEY_BUNDLE_TYPE }, 177 | { tag: 'identity', attrs: { }, content: creds.signedIdentityKey.public }, 178 | { tag: 'list', attrs: { }, content: Object.keys(preKeys).map(k => xmppPreKey(preKeys[+k], +k)) }, 179 | xmppSignedPreKey(creds.signedPreKey) 180 | ] 181 | } 182 | 183 | return { update, node } 184 | } 185 | -------------------------------------------------------------------------------- /src/Utils/use-multi-file-auth-state.ts: -------------------------------------------------------------------------------- 1 | import { Mutex } from 'async-mutex' 2 | import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises' 3 | import { join } from 'path' 4 | import { proto } from '../../WAProto' 5 | import { AuthenticationCreds, AuthenticationState, SignalDataTypeMap } from '../Types' 6 | import { initAuthCreds } from './auth-utils' 7 | import { BufferJSON } from './generics' 8 | 9 | // We need to lock files due to the fact that we are using async functions to read and write files 10 | // https://github.com/WhiskeySockets/Baileys/issues/794 11 | // https://github.com/nodejs/node/issues/26338 12 | // Use a Map to store mutexes for each file path 13 | const fileLocks = new Map() 14 | 15 | // Get or create a mutex for a specific file path 16 | const getFileLock = (path: string): Mutex => { 17 | let mutex = fileLocks.get(path) 18 | if(!mutex) { 19 | mutex = new Mutex() 20 | fileLocks.set(path, mutex) 21 | } 22 | 23 | return mutex 24 | } 25 | 26 | /** 27 | * stores the full authentication state in a single folder. 28 | * Far more efficient than singlefileauthstate 29 | * 30 | * Again, I wouldn't endorse this for any production level use other than perhaps a bot. 31 | * Would recommend writing an auth state for use with a proper SQL or No-SQL DB 32 | * */ 33 | export const useMultiFileAuthState = async(folder: string): Promise<{ state: AuthenticationState, saveCreds: () => Promise }> => { 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | const writeData = async(data: any, file: string) => { 36 | const filePath = join(folder, fixFileName(file)!) 37 | const mutex = getFileLock(filePath) 38 | 39 | return mutex.acquire().then(async(release) => { 40 | try { 41 | await writeFile(filePath, JSON.stringify(data, BufferJSON.replacer)) 42 | } finally { 43 | release() 44 | } 45 | }) 46 | } 47 | 48 | const readData = async(file: string) => { 49 | try { 50 | const filePath = join(folder, fixFileName(file)!) 51 | const mutex = getFileLock(filePath) 52 | 53 | return await mutex.acquire().then(async(release) => { 54 | try { 55 | const data = await readFile(filePath, { encoding: 'utf-8' }) 56 | return JSON.parse(data, BufferJSON.reviver) 57 | } finally { 58 | release() 59 | } 60 | }) 61 | } catch(error) { 62 | return null 63 | } 64 | } 65 | 66 | const removeData = async(file: string) => { 67 | try { 68 | const filePath = join(folder, fixFileName(file)!) 69 | const mutex = getFileLock(filePath) 70 | 71 | return mutex.acquire().then(async(release) => { 72 | try { 73 | await unlink(filePath) 74 | } catch{ 75 | } finally { 76 | release() 77 | } 78 | }) 79 | } catch{ 80 | } 81 | } 82 | 83 | const folderInfo = await stat(folder).catch(() => { }) 84 | if(folderInfo) { 85 | if(!folderInfo.isDirectory()) { 86 | throw new Error(`found something that is not a directory at ${folder}, either delete it or specify a different location`) 87 | } 88 | } else { 89 | await mkdir(folder, { recursive: true }) 90 | } 91 | 92 | const fixFileName = (file?: string) => file?.replace(/\//g, '__')?.replace(/:/g, '-') 93 | 94 | const creds: AuthenticationCreds = await readData('creds.json') || initAuthCreds() 95 | 96 | return { 97 | state: { 98 | creds, 99 | keys: { 100 | get: async(type, ids) => { 101 | const data: { [_: string]: SignalDataTypeMap[typeof type] } = { } 102 | await Promise.all( 103 | ids.map( 104 | async id => { 105 | let value = await readData(`${type}-${id}.json`) 106 | if(type === 'app-state-sync-key' && value) { 107 | value = proto.Message.AppStateSyncKeyData.fromObject(value) 108 | } 109 | 110 | data[id] = value 111 | } 112 | ) 113 | ) 114 | 115 | return data 116 | }, 117 | set: async(data) => { 118 | const tasks: Promise[] = [] 119 | for(const category in data) { 120 | for(const id in data[category]) { 121 | const value = data[category][id] 122 | const file = `${category}-${id}.json` 123 | tasks.push(value ? writeData(value, file) : removeData(file)) 124 | } 125 | } 126 | 127 | await Promise.all(tasks) 128 | } 129 | } 130 | }, 131 | saveCreds: async() => { 132 | return writeData(creds, 'creds.json') 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /src/Utils/validate-connection.ts: -------------------------------------------------------------------------------- 1 | import { Boom } from '@hapi/boom' 2 | import { createHash } from 'crypto' 3 | import { proto } from '../../WAProto' 4 | import { KEY_BUNDLE_TYPE } from '../Defaults' 5 | import type { AuthenticationCreds, SignalCreds, SocketConfig } from '../Types' 6 | import { BinaryNode, getBinaryNodeChild, jidDecode, S_WHATSAPP_NET } from '../WABinary' 7 | import { Curve, hmacSign } from './crypto' 8 | import { encodeBigEndian } from './generics' 9 | import { createSignalIdentity } from './signal' 10 | 11 | const getUserAgent = (config: SocketConfig): proto.ClientPayload.IUserAgent => { 12 | return { 13 | appVersion: { 14 | primary: config.version[0], 15 | secondary: config.version[1], 16 | tertiary: config.version[2], 17 | }, 18 | platform: proto.ClientPayload.UserAgent.Platform.WEB, 19 | releaseChannel: proto.ClientPayload.UserAgent.ReleaseChannel.RELEASE, 20 | osVersion: '0.1', 21 | device: 'Desktop', 22 | osBuildNumber: '0.1', 23 | localeLanguageIso6391: 'en', 24 | mnc: '000', 25 | mcc: '000', 26 | localeCountryIso31661Alpha2: config.countryCode, 27 | } 28 | } 29 | 30 | const PLATFORM_MAP = { 31 | 'Mac OS': proto.ClientPayload.WebInfo.WebSubPlatform.DARWIN, 32 | 'Windows': proto.ClientPayload.WebInfo.WebSubPlatform.WIN32 33 | } 34 | 35 | const getWebInfo = (config: SocketConfig): proto.ClientPayload.IWebInfo => { 36 | let webSubPlatform = proto.ClientPayload.WebInfo.WebSubPlatform.WEB_BROWSER 37 | if(config.syncFullHistory && PLATFORM_MAP[config.browser[0]]) { 38 | webSubPlatform = PLATFORM_MAP[config.browser[0]] 39 | } 40 | 41 | return { webSubPlatform } 42 | } 43 | 44 | 45 | const getClientPayload = (config: SocketConfig) => { 46 | const payload: proto.IClientPayload = { 47 | connectType: proto.ClientPayload.ConnectType.WIFI_UNKNOWN, 48 | connectReason: proto.ClientPayload.ConnectReason.USER_ACTIVATED, 49 | userAgent: getUserAgent(config), 50 | } 51 | 52 | payload.webInfo = getWebInfo(config) 53 | 54 | return payload 55 | } 56 | 57 | 58 | export const generateLoginNode = (userJid: string, config: SocketConfig): proto.IClientPayload => { 59 | const { user, device } = jidDecode(userJid)! 60 | const payload: proto.IClientPayload = { 61 | ...getClientPayload(config), 62 | passive: false, 63 | pull: true, 64 | username: +user, 65 | device: device, 66 | } 67 | return proto.ClientPayload.fromObject(payload) 68 | } 69 | 70 | const getPlatformType = (platform: string): proto.DeviceProps.PlatformType => { 71 | const platformType = platform.toUpperCase() 72 | return proto.DeviceProps.PlatformType[platformType] || proto.DeviceProps.PlatformType.DESKTOP 73 | } 74 | 75 | export const generateRegistrationNode = ( 76 | { registrationId, signedPreKey, signedIdentityKey }: SignalCreds, 77 | config: SocketConfig 78 | ) => { 79 | // the app version needs to be md5 hashed 80 | // and passed in 81 | const appVersionBuf = createHash('md5') 82 | .update(config.version.join('.')) // join as string 83 | .digest() 84 | 85 | const companion: proto.IDeviceProps = { 86 | os: config.browser[0], 87 | platformType: getPlatformType(config.browser[1]), 88 | requireFullSync: config.syncFullHistory, 89 | } 90 | 91 | const companionProto = proto.DeviceProps.encode(companion).finish() 92 | 93 | const registerPayload: proto.IClientPayload = { 94 | ...getClientPayload(config), 95 | passive: false, 96 | pull: false, 97 | devicePairingData: { 98 | buildHash: appVersionBuf, 99 | deviceProps: companionProto, 100 | eRegid: encodeBigEndian(registrationId), 101 | eKeytype: KEY_BUNDLE_TYPE, 102 | eIdent: signedIdentityKey.public, 103 | eSkeyId: encodeBigEndian(signedPreKey.keyId, 3), 104 | eSkeyVal: signedPreKey.keyPair.public, 105 | eSkeySig: signedPreKey.signature, 106 | }, 107 | } 108 | 109 | return proto.ClientPayload.fromObject(registerPayload) 110 | } 111 | 112 | export const configureSuccessfulPairing = ( 113 | stanza: BinaryNode, 114 | { advSecretKey, signedIdentityKey, signalIdentities }: Pick 115 | ) => { 116 | const msgId = stanza.attrs.id 117 | 118 | const pairSuccessNode = getBinaryNodeChild(stanza, 'pair-success') 119 | 120 | const deviceIdentityNode = getBinaryNodeChild(pairSuccessNode, 'device-identity') 121 | const platformNode = getBinaryNodeChild(pairSuccessNode, 'platform') 122 | const deviceNode = getBinaryNodeChild(pairSuccessNode, 'device') 123 | const businessNode = getBinaryNodeChild(pairSuccessNode, 'biz') 124 | 125 | if(!deviceIdentityNode || !deviceNode) { 126 | throw new Boom('Missing device-identity or device in pair success node', { data: stanza }) 127 | } 128 | 129 | const bizName = businessNode?.attrs.name 130 | const jid = deviceNode.attrs.jid 131 | 132 | const { details, hmac } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityNode.content as Buffer) 133 | // check HMAC matches 134 | const advSign = hmacSign(details!, Buffer.from(advSecretKey, 'base64')) 135 | if(Buffer.compare(hmac!, advSign) !== 0) { 136 | throw new Boom('Invalid account signature') 137 | } 138 | 139 | const account = proto.ADVSignedDeviceIdentity.decode(details!) 140 | const { accountSignatureKey, accountSignature, details: deviceDetails } = account 141 | // verify the device signature matches 142 | const accountMsg = Buffer.concat([ Buffer.from([6, 0]), deviceDetails!, signedIdentityKey.public ]) 143 | if(!Curve.verify(accountSignatureKey!, accountMsg, accountSignature!)) { 144 | throw new Boom('Failed to verify account signature') 145 | } 146 | 147 | // sign the details with our identity key 148 | const deviceMsg = Buffer.concat([ Buffer.from([6, 1]), deviceDetails!, signedIdentityKey.public, accountSignatureKey! ]) 149 | account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg) 150 | 151 | const identity = createSignalIdentity(jid, accountSignatureKey!) 152 | const accountEnc = encodeSignedDeviceIdentity(account, false) 153 | 154 | const deviceIdentity = proto.ADVDeviceIdentity.decode(account.details!) 155 | 156 | const reply: BinaryNode = { 157 | tag: 'iq', 158 | attrs: { 159 | to: S_WHATSAPP_NET, 160 | type: 'result', 161 | id: msgId, 162 | }, 163 | content: [ 164 | { 165 | tag: 'pair-device-sign', 166 | attrs: { }, 167 | content: [ 168 | { 169 | tag: 'device-identity', 170 | attrs: { 'key-index': deviceIdentity.keyIndex!.toString() }, 171 | content: accountEnc 172 | } 173 | ] 174 | } 175 | ] 176 | } 177 | 178 | const authUpdate: Partial = { 179 | account, 180 | me: { id: jid, name: bizName }, 181 | signalIdentities: [ 182 | ...(signalIdentities || []), 183 | identity 184 | ], 185 | platform: platformNode?.attrs.name 186 | } 187 | 188 | return { 189 | creds: authUpdate, 190 | reply 191 | } 192 | } 193 | 194 | export const encodeSignedDeviceIdentity = ( 195 | account: proto.IADVSignedDeviceIdentity, 196 | includeSignatureKey: boolean 197 | ) => { 198 | account = { ...account } 199 | // set to null if we are not to include the signature key 200 | // or if we are including the signature key but it is empty 201 | if(!includeSignatureKey || !account.accountSignatureKey?.length) { 202 | account.accountSignatureKey = null 203 | } 204 | 205 | return proto.ADVSignedDeviceIdentity 206 | .encode(account) 207 | .finish() 208 | } 209 | -------------------------------------------------------------------------------- /src/WABinary/encode.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as constants from './constants' 3 | import { FullJid, jidDecode } from './jid-utils' 4 | import type { BinaryNode, BinaryNodeCodingOptions } from './types' 5 | 6 | export const encodeBinaryNode = ( 7 | node: BinaryNode, 8 | opts: Pick = constants, 9 | buffer: number[] = [0] 10 | ): Buffer => { 11 | const encoded = encodeBinaryNodeInner(node, opts, buffer) 12 | return Buffer.from(encoded) 13 | } 14 | 15 | const encodeBinaryNodeInner = ( 16 | { tag, attrs, content }: BinaryNode, 17 | opts: Pick, 18 | buffer: number[] 19 | ): number[] => { 20 | const { TAGS, TOKEN_MAP } = opts 21 | 22 | const pushByte = (value: number) => buffer.push(value & 0xff) 23 | 24 | const pushInt = (value: number, n: number, littleEndian = false) => { 25 | for(let i = 0; i < n; i++) { 26 | const curShift = littleEndian ? i : n - 1 - i 27 | buffer.push((value >> (curShift * 8)) & 0xff) 28 | } 29 | } 30 | 31 | const pushBytes = (bytes: Uint8Array | Buffer | number[]) => { 32 | for(const b of bytes) { 33 | buffer.push(b) 34 | } 35 | } 36 | 37 | const pushInt16 = (value: number) => { 38 | pushBytes([(value >> 8) & 0xff, value & 0xff]) 39 | } 40 | 41 | const pushInt20 = (value: number) => ( 42 | pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff]) 43 | ) 44 | const writeByteLength = (length: number) => { 45 | if(length >= 4294967296) { 46 | throw new Error('string too large to encode: ' + length) 47 | } 48 | 49 | if(length >= 1 << 20) { 50 | pushByte(TAGS.BINARY_32) 51 | pushInt(length, 4) // 32 bit integer 52 | } else if(length >= 256) { 53 | pushByte(TAGS.BINARY_20) 54 | pushInt20(length) 55 | } else { 56 | pushByte(TAGS.BINARY_8) 57 | pushByte(length) 58 | } 59 | } 60 | 61 | const writeStringRaw = (str: string) => { 62 | const bytes = Buffer.from (str, 'utf-8') 63 | writeByteLength(bytes.length) 64 | pushBytes(bytes) 65 | } 66 | 67 | const writeJid = ({ domainType, device, user, server }: FullJid) => { 68 | if(typeof device !== 'undefined') { 69 | pushByte(TAGS.AD_JID) 70 | pushByte(domainType || 0) 71 | pushByte(device || 0) 72 | writeString(user) 73 | } else { 74 | pushByte(TAGS.JID_PAIR) 75 | if(user.length) { 76 | writeString(user) 77 | } else { 78 | pushByte(TAGS.LIST_EMPTY) 79 | } 80 | 81 | writeString(server) 82 | } 83 | } 84 | 85 | const packNibble = (char: string) => { 86 | switch (char) { 87 | case '-': 88 | return 10 89 | case '.': 90 | return 11 91 | case '\0': 92 | return 15 93 | default: 94 | if(char >= '0' && char <= '9') { 95 | return char.charCodeAt(0) - '0'.charCodeAt(0) 96 | } 97 | 98 | throw new Error(`invalid byte for nibble "${char}"`) 99 | } 100 | } 101 | 102 | const packHex = (char: string) => { 103 | if(char >= '0' && char <= '9') { 104 | return char.charCodeAt(0) - '0'.charCodeAt(0) 105 | } 106 | 107 | if(char >= 'A' && char <= 'F') { 108 | return 10 + char.charCodeAt(0) - 'A'.charCodeAt(0) 109 | } 110 | 111 | if(char >= 'a' && char <= 'f') { 112 | return 10 + char.charCodeAt(0) - 'a'.charCodeAt(0) 113 | } 114 | 115 | if(char === '\0') { 116 | return 15 117 | } 118 | 119 | throw new Error(`Invalid hex char "${char}"`) 120 | } 121 | 122 | const writePackedBytes = (str: string, type: 'nibble' | 'hex') => { 123 | if(str.length > TAGS.PACKED_MAX) { 124 | throw new Error('Too many bytes to pack') 125 | } 126 | 127 | pushByte(type === 'nibble' ? TAGS.NIBBLE_8 : TAGS.HEX_8) 128 | 129 | let roundedLength = Math.ceil(str.length / 2.0) 130 | if(str.length % 2 !== 0) { 131 | roundedLength |= 128 132 | } 133 | 134 | pushByte(roundedLength) 135 | const packFunction = type === 'nibble' ? packNibble : packHex 136 | 137 | const packBytePair = (v1: string, v2: string) => { 138 | const result = (packFunction(v1) << 4) | packFunction(v2) 139 | return result 140 | } 141 | 142 | const strLengthHalf = Math.floor(str.length / 2) 143 | for(let i = 0; i < strLengthHalf;i++) { 144 | pushByte(packBytePair(str[2 * i], str[2 * i + 1])) 145 | } 146 | 147 | if(str.length % 2 !== 0) { 148 | pushByte(packBytePair(str[str.length - 1], '\x00')) 149 | } 150 | } 151 | 152 | const isNibble = (str?: string) => { 153 | if(!str || str.length > TAGS.PACKED_MAX) { 154 | return false 155 | } 156 | 157 | for(const char of str) { 158 | const isInNibbleRange = char >= '0' && char <= '9' 159 | if(!isInNibbleRange && char !== '-' && char !== '.') { 160 | return false 161 | } 162 | } 163 | 164 | return true 165 | } 166 | 167 | const isHex = (str?: string) => { 168 | if(!str || str.length > TAGS.PACKED_MAX) { 169 | return false 170 | } 171 | 172 | for(const char of str) { 173 | const isInNibbleRange = char >= '0' && char <= '9' 174 | if(!isInNibbleRange && !(char >= 'A' && char <= 'F')) { 175 | return false 176 | } 177 | } 178 | 179 | return true 180 | } 181 | 182 | const writeString = (str?: string) => { 183 | if(str === undefined || str === null) { 184 | pushByte(TAGS.LIST_EMPTY) 185 | return 186 | } 187 | 188 | const tokenIndex = TOKEN_MAP[str] 189 | if(tokenIndex) { 190 | if(typeof tokenIndex.dict === 'number') { 191 | pushByte(TAGS.DICTIONARY_0 + tokenIndex.dict) 192 | } 193 | 194 | pushByte(tokenIndex.index) 195 | } else if(isNibble(str)) { 196 | writePackedBytes(str, 'nibble') 197 | } else if(isHex(str)) { 198 | writePackedBytes(str, 'hex') 199 | } else if(str) { 200 | const decodedJid = jidDecode(str) 201 | if(decodedJid) { 202 | writeJid(decodedJid) 203 | } else { 204 | writeStringRaw(str) 205 | } 206 | } 207 | } 208 | 209 | const writeListStart = (listSize: number) => { 210 | if(listSize === 0) { 211 | pushByte(TAGS.LIST_EMPTY) 212 | } else if(listSize < 256) { 213 | pushBytes([TAGS.LIST_8, listSize]) 214 | } else { 215 | pushByte(TAGS.LIST_16) 216 | pushInt16(listSize) 217 | } 218 | } 219 | 220 | if(!tag) { 221 | throw new Error('Invalid node: tag cannot be undefined') 222 | } 223 | 224 | const validAttributes = Object.keys(attrs || {}).filter(k => ( 225 | typeof attrs[k] !== 'undefined' && attrs[k] !== null 226 | )) 227 | 228 | writeListStart(2 * validAttributes.length + 1 + (typeof content !== 'undefined' ? 1 : 0)) 229 | writeString(tag) 230 | 231 | for(const key of validAttributes) { 232 | if(typeof attrs[key] === 'string') { 233 | writeString(key) 234 | writeString(attrs[key]) 235 | } 236 | } 237 | 238 | if(typeof content === 'string') { 239 | writeString(content) 240 | } else if(Buffer.isBuffer(content) || content instanceof Uint8Array) { 241 | writeByteLength(content.length) 242 | pushBytes(content) 243 | } else if(Array.isArray(content)) { 244 | const validContent = content.filter(item => item && (item.tag || Buffer.isBuffer(item) || item instanceof Uint8Array || typeof item === 'string') 245 | ) 246 | writeListStart(validContent.length) 247 | for(const item of validContent) { 248 | encodeBinaryNodeInner(item, opts, buffer) 249 | } 250 | } else if(typeof content === 'undefined') { 251 | // do nothing 252 | } else { 253 | throw new Error(`invalid children for header "${tag}": ${content} (${typeof content})`) 254 | } 255 | 256 | return buffer 257 | } 258 | -------------------------------------------------------------------------------- /src/WABinary/generic-utils.ts: -------------------------------------------------------------------------------- 1 | import { Boom } from '@hapi/boom' 2 | import { proto } from '../../WAProto' 3 | import { BinaryNode } from './types' 4 | 5 | // some extra useful utilities 6 | 7 | export const getBinaryNodeChildren = (node: BinaryNode | undefined, childTag: string) => { 8 | if(Array.isArray(node?.content)) { 9 | return node.content.filter(item => item.tag === childTag) 10 | } 11 | 12 | return [] 13 | } 14 | 15 | export const getAllBinaryNodeChildren = ({ content }: BinaryNode) => { 16 | if(Array.isArray(content)) { 17 | return content 18 | } 19 | 20 | return [] 21 | } 22 | 23 | export const getBinaryNodeChild = (node: BinaryNode | undefined, childTag: string) => { 24 | if(Array.isArray(node?.content)) { 25 | return node?.content.find(item => item.tag === childTag) 26 | } 27 | } 28 | 29 | export const getBinaryNodeChildBuffer = (node: BinaryNode | undefined, childTag: string) => { 30 | const child = getBinaryNodeChild(node, childTag)?.content 31 | if(Buffer.isBuffer(child) || child instanceof Uint8Array) { 32 | return child 33 | } 34 | } 35 | 36 | export const getBinaryNodeChildString = (node: BinaryNode | undefined, childTag: string) => { 37 | const child = getBinaryNodeChild(node, childTag)?.content 38 | if(Buffer.isBuffer(child) || child instanceof Uint8Array) { 39 | return Buffer.from(child).toString('utf-8') 40 | } else if(typeof child === 'string') { 41 | return child 42 | } 43 | } 44 | 45 | export const getBinaryNodeChildUInt = (node: BinaryNode, childTag: string, length: number) => { 46 | const buff = getBinaryNodeChildBuffer(node, childTag) 47 | if(buff) { 48 | return bufferToUInt(buff, length) 49 | } 50 | } 51 | 52 | export const assertNodeErrorFree = (node: BinaryNode) => { 53 | const errNode = getBinaryNodeChild(node, 'error') 54 | if(errNode) { 55 | throw new Boom(errNode.attrs.text || 'Unknown error', { data: +errNode.attrs.code }) 56 | } 57 | } 58 | 59 | export const reduceBinaryNodeToDictionary = (node: BinaryNode, tag: string) => { 60 | const nodes = getBinaryNodeChildren(node, tag) 61 | const dict = nodes.reduce( 62 | (dict, { attrs }) => { 63 | dict[attrs.name || attrs.config_code] = attrs.value || attrs.config_value 64 | return dict 65 | }, { } as { [_: string]: string } 66 | ) 67 | return dict 68 | } 69 | 70 | export const getBinaryNodeMessages = ({ content }: BinaryNode) => { 71 | const msgs: proto.WebMessageInfo[] = [] 72 | if(Array.isArray(content)) { 73 | for(const item of content) { 74 | if(item.tag === 'message') { 75 | msgs.push(proto.WebMessageInfo.decode(item.content as Buffer)) 76 | } 77 | } 78 | } 79 | 80 | return msgs 81 | } 82 | 83 | function bufferToUInt(e: Uint8Array | Buffer, t: number) { 84 | let a = 0 85 | for(let i = 0; i < t; i++) { 86 | a = 256 * a + e[i] 87 | } 88 | 89 | return a 90 | } 91 | 92 | const tabs = (n: number) => '\t'.repeat(n) 93 | 94 | export function binaryNodeToString(node: BinaryNode | BinaryNode['content'], i = 0) { 95 | if(!node) { 96 | return node 97 | } 98 | 99 | if(typeof node === 'string') { 100 | return tabs(i) + node 101 | } 102 | 103 | if(node instanceof Uint8Array) { 104 | return tabs(i) + Buffer.from(node).toString('hex') 105 | } 106 | 107 | if(Array.isArray(node)) { 108 | return node.map((x) => tabs(i + 1) + binaryNodeToString(x, i + 1)).join('\n') 109 | } 110 | 111 | const children = binaryNodeToString(node.content, i + 1) 112 | 113 | const tag = `<${node.tag} ${Object.entries(node.attrs || {}) 114 | .filter(([, v]) => v !== undefined) 115 | .map(([k, v]) => `${k}='${v}'`) 116 | .join(' ')}` 117 | 118 | const content: string = children ? `>\n${children}\n${tabs(i)}` : '/>' 119 | 120 | return tag + content 121 | } -------------------------------------------------------------------------------- /src/WABinary/index.ts: -------------------------------------------------------------------------------- 1 | export * from './encode' 2 | export * from './decode' 3 | export * from './generic-utils' 4 | export * from './jid-utils' 5 | export * from './types' -------------------------------------------------------------------------------- /src/WABinary/jid-utils.ts: -------------------------------------------------------------------------------- 1 | export const S_WHATSAPP_NET = '@s.whatsapp.net' 2 | export const OFFICIAL_BIZ_JID = '16505361212@c.us' 3 | export const SERVER_JID = 'server@c.us' 4 | export const PSA_WID = '0@c.us' 5 | export const STORIES_JID = 'status@broadcast' 6 | export const META_AI_JID = '13135550002@c.us' 7 | 8 | export type JidServer = 'c.us' | 'g.us' | 'broadcast' | 's.whatsapp.net' | 'call' | 'lid' | 'newsletter' | 'bot' 9 | 10 | export type JidWithDevice = { 11 | user: string 12 | device?: number 13 | } 14 | 15 | export type FullJid = JidWithDevice & { 16 | server: JidServer 17 | domainType?: number 18 | } 19 | 20 | 21 | export const jidEncode = (user: string | number | null, server: JidServer, device?: number, agent?: number) => { 22 | return `${user || ''}${!!agent ? `_${agent}` : ''}${!!device ? `:${device}` : ''}@${server}` 23 | } 24 | 25 | export const jidDecode = (jid: string | undefined): FullJid | undefined => { 26 | const sepIdx = typeof jid === 'string' ? jid.indexOf('@') : -1 27 | if(sepIdx < 0) { 28 | return undefined 29 | } 30 | 31 | const server = jid!.slice(sepIdx + 1) 32 | const userCombined = jid!.slice(0, sepIdx) 33 | 34 | const [userAgent, device] = userCombined.split(':') 35 | const user = userAgent.split('_')[0] 36 | 37 | return { 38 | server: server as JidServer, 39 | user, 40 | domainType: server === 'lid' ? 1 : 0, 41 | device: device ? +device : undefined 42 | } 43 | } 44 | 45 | /** is the jid a user */ 46 | export const areJidsSameUser = (jid1: string | undefined, jid2: string | undefined) => ( 47 | jidDecode(jid1)?.user === jidDecode(jid2)?.user 48 | ) 49 | /** is the jid Meta IA */ 50 | export const isJidMetaIa = (jid: string | undefined) => (jid?.endsWith('@bot')) 51 | /** is the jid a user */ 52 | export const isJidUser = (jid: string | undefined) => (jid?.endsWith('@s.whatsapp.net')) 53 | /** is the jid a group */ 54 | export const isLidUser = (jid: string | undefined) => (jid?.endsWith('@lid')) 55 | /** is the jid a broadcast */ 56 | export const isJidBroadcast = (jid: string | undefined) => (jid?.endsWith('@broadcast')) 57 | /** is the jid a group */ 58 | export const isJidGroup = (jid: string | undefined) => (jid?.endsWith('@g.us')) 59 | /** is the jid the status broadcast */ 60 | export const isJidStatusBroadcast = (jid: string) => jid === 'status@broadcast' 61 | /** is the jid a newsletter */ 62 | export const isJidNewsletter = (jid: string | undefined) => (jid?.endsWith('@newsletter')) 63 | 64 | const botRegexp = /^1313555\d{4}$|^131655500\d{2}$/ 65 | 66 | export const isJidBot = (jid: string | undefined) => (jid && botRegexp.test(jid.split('@')[0]) && jid.endsWith('@c.us')) 67 | 68 | export const jidNormalizedUser = (jid: string | undefined) => { 69 | const result = jidDecode(jid) 70 | if(!result) { 71 | return '' 72 | } 73 | 74 | const { user, server } = result 75 | return jidEncode(user, server === 'c.us' ? 's.whatsapp.net' : server as JidServer) 76 | } 77 | -------------------------------------------------------------------------------- /src/WABinary/types.ts: -------------------------------------------------------------------------------- 1 | import * as constants from './constants' 2 | /** 3 | * the binary node WA uses internally for communication 4 | * 5 | * this is manipulated soley as an object and it does not have any functions. 6 | * This is done for easy serialization, to prevent running into issues with prototypes & 7 | * to maintain functional code structure 8 | * */ 9 | export type BinaryNode = { 10 | tag: string 11 | attrs: { [key: string]: string } 12 | content?: BinaryNode[] | string | Uint8Array 13 | } 14 | export type BinaryNodeAttributes = BinaryNode['attrs'] 15 | export type BinaryNodeData = BinaryNode['content'] 16 | 17 | export type BinaryNodeCodingOptions = typeof constants -------------------------------------------------------------------------------- /src/WAM/BinaryInfo.ts: -------------------------------------------------------------------------------- 1 | import { EventInputType } from './constants' 2 | 3 | export class BinaryInfo { 4 | protocolVersion = 5 5 | sequence = 0 6 | events = [] as EventInputType[] 7 | buffer: Buffer[] = [] 8 | 9 | constructor(options: Partial = {}) { 10 | Object.assign(this, options) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/WAM/encode.ts: -------------------------------------------------------------------------------- 1 | import { BinaryInfo } from './BinaryInfo' 2 | import { FLAG_BYTE, FLAG_EVENT, FLAG_EXTENDED, FLAG_FIELD, FLAG_GLOBAL, Value, WEB_EVENTS, WEB_GLOBALS } from './constants' 3 | 4 | const getHeaderBitLength = (key: number) => (key < 256 ? 2 : 3) 5 | 6 | export const encodeWAM = (binaryInfo: BinaryInfo) => { 7 | binaryInfo.buffer = [] 8 | 9 | encodeWAMHeader(binaryInfo) 10 | encodeEvents(binaryInfo) 11 | 12 | console.log(binaryInfo.buffer) 13 | const totalSize = binaryInfo.buffer 14 | .map((a) => a.length) 15 | .reduce((a, b) => a + b) 16 | const buffer = Buffer.alloc(totalSize) 17 | let offset = 0 18 | for(const buffer_ of binaryInfo.buffer) { 19 | buffer_.copy(buffer, offset) 20 | offset += buffer_.length 21 | } 22 | 23 | return buffer 24 | } 25 | 26 | function encodeWAMHeader(binaryInfo: BinaryInfo) { 27 | const headerBuffer = Buffer.alloc(8) // starting buffer 28 | headerBuffer.write('WAM', 0, 'utf8') 29 | headerBuffer.writeUInt8(binaryInfo.protocolVersion, 3) 30 | headerBuffer.writeUInt8(1, 4) //random flag 31 | headerBuffer.writeUInt16BE(binaryInfo.sequence, 5) 32 | headerBuffer.writeUInt8(0, 7) // regular channel 33 | 34 | binaryInfo.buffer.push(headerBuffer) 35 | } 36 | 37 | function encodeGlobalAttributes(binaryInfo: BinaryInfo, globals: {[key: string]: Value}) { 38 | for(const [key, _value] of Object.entries(globals)) { 39 | const id = WEB_GLOBALS.find(a => a?.name === key)!.id 40 | let value = _value 41 | if(typeof value === 'boolean') { 42 | value = value ? 1 : 0 43 | } 44 | 45 | binaryInfo.buffer.push(serializeData(id, value, FLAG_GLOBAL)) 46 | } 47 | } 48 | 49 | function encodeEvents(binaryInfo: BinaryInfo) { 50 | for(const [ 51 | name, 52 | { props, globals }, 53 | ] of binaryInfo.events.map((a) => Object.entries(a)[0])) { 54 | encodeGlobalAttributes(binaryInfo, globals) 55 | const event = WEB_EVENTS.find((a) => a.name === name)! 56 | 57 | const props_ = Object.entries(props) 58 | 59 | let extended = false 60 | 61 | for(const [, value] of props_) { 62 | extended ||= value !== null 63 | } 64 | 65 | const eventFlag = extended ? FLAG_EVENT : FLAG_EVENT | FLAG_EXTENDED 66 | binaryInfo.buffer.push(serializeData(event.id, -event.weight, eventFlag)) 67 | 68 | for(let i = 0; i < props_.length; i++) { 69 | const [key, _value] = props_[i] 70 | const id = (event.props)[key][0] 71 | extended = i < (props_.length - 1) 72 | let value = _value 73 | if(typeof value === 'boolean') { 74 | value = value ? 1 : 0 75 | } 76 | 77 | const fieldFlag = extended ? FLAG_EVENT : FLAG_FIELD | FLAG_EXTENDED 78 | binaryInfo.buffer.push(serializeData(id, value, fieldFlag)) 79 | } 80 | } 81 | } 82 | 83 | 84 | function serializeData(key: number, value: Value, flag: number): Buffer { 85 | const bufferLength = getHeaderBitLength(key) 86 | let buffer: Buffer 87 | let offset = 0 88 | if(value === null) { 89 | if(flag === FLAG_GLOBAL) { 90 | buffer = Buffer.alloc(bufferLength) 91 | offset = serializeHeader(buffer, offset, key, flag) 92 | return buffer 93 | } 94 | } else if(typeof value === 'number' && Number.isInteger(value)) { 95 | // is number 96 | if(value === 0 || value === 1) { 97 | buffer = Buffer.alloc(bufferLength) 98 | offset = serializeHeader(buffer, offset, key, flag | ((value + 1) << 4)) 99 | return buffer 100 | } else if(-128 <= value && value < 128) { 101 | buffer = Buffer.alloc(bufferLength + 1) 102 | offset = serializeHeader(buffer, offset, key, flag | (3 << 4)) 103 | buffer.writeInt8(value, offset) 104 | return buffer 105 | } else if(-32768 <= value && value < 32768) { 106 | buffer = Buffer.alloc(bufferLength + 2) 107 | offset = serializeHeader(buffer, offset, key, flag | (4 << 4)) 108 | buffer.writeInt16LE(value, offset) 109 | return buffer 110 | } else if(-2147483648 <= value && value < 2147483648) { 111 | buffer = Buffer.alloc(bufferLength + 4) 112 | offset = serializeHeader(buffer, offset, key, flag | (5 << 4)) 113 | buffer.writeInt32LE(value, offset) 114 | return buffer 115 | } else { 116 | buffer = Buffer.alloc(bufferLength + 8) 117 | offset = serializeHeader(buffer, offset, key, flag | (7 << 4)) 118 | buffer.writeDoubleLE(value, offset) 119 | return buffer 120 | } 121 | } else if(typeof value === 'number') { 122 | // is float 123 | buffer = Buffer.alloc(bufferLength + 8) 124 | offset = serializeHeader(buffer, offset, key, flag | (7 << 4)) 125 | buffer.writeDoubleLE(value, offset) 126 | return buffer 127 | } else if(typeof value === 'string') { 128 | // is string 129 | const utf8Bytes = Buffer.byteLength(value, 'utf8') 130 | if(utf8Bytes < 256) { 131 | buffer = Buffer.alloc(bufferLength + 1 + utf8Bytes) 132 | offset = serializeHeader(buffer, offset, key, flag | (8 << 4)) 133 | buffer.writeUint8(utf8Bytes, offset++) 134 | } else if(utf8Bytes < 65536) { 135 | buffer = Buffer.alloc(bufferLength + 2 + utf8Bytes) 136 | offset = serializeHeader(buffer, offset, key, flag | (9 << 4)) 137 | buffer.writeUInt16LE(utf8Bytes, offset) 138 | offset += 2 139 | } else { 140 | buffer = Buffer.alloc(bufferLength + 4 + utf8Bytes) 141 | offset = serializeHeader(buffer, offset, key, flag | (10 << 4)) 142 | buffer.writeUInt32LE(utf8Bytes, offset) 143 | offset += 4 144 | } 145 | 146 | buffer.write(value, offset, 'utf8') 147 | return buffer 148 | } 149 | 150 | throw 'missing' 151 | } 152 | 153 | function serializeHeader( 154 | buffer: Buffer, 155 | offset: number, 156 | key: number, 157 | flag: number 158 | ) { 159 | if(key < 256) { 160 | buffer.writeUInt8(flag, offset) 161 | offset += 1 162 | buffer.writeUInt8(key, offset) 163 | offset += 1 164 | } else { 165 | buffer.writeUInt8(flag | FLAG_BYTE, offset) 166 | offset += 1 167 | buffer.writeUInt16LE(key, offset) 168 | offset += 2 169 | } 170 | 171 | return offset 172 | } -------------------------------------------------------------------------------- /src/WAM/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants' 2 | export * from './encode' 3 | export * from './BinaryInfo' -------------------------------------------------------------------------------- /src/WAUSync/Protocols/USyncContactProtocol.ts: -------------------------------------------------------------------------------- 1 | import { USyncQueryProtocol } from '../../Types/USync' 2 | import { assertNodeErrorFree, BinaryNode } from '../../WABinary' 3 | import { USyncUser } from '../USyncUser' 4 | 5 | export class USyncContactProtocol implements USyncQueryProtocol { 6 | name = 'contact' 7 | 8 | getQueryElement(): BinaryNode { 9 | return { 10 | tag: 'contact', 11 | attrs: {}, 12 | } 13 | } 14 | 15 | getUserElement(user: USyncUser): BinaryNode { 16 | //TODO: Implement type / username fields (not yet supported) 17 | return { 18 | tag: 'contact', 19 | attrs: {}, 20 | content: user.phone, 21 | } 22 | } 23 | 24 | parser(node: BinaryNode): boolean { 25 | if(node.tag === 'contact') { 26 | assertNodeErrorFree(node) 27 | return node?.attrs?.type === 'in' 28 | } 29 | 30 | return false 31 | } 32 | } -------------------------------------------------------------------------------- /src/WAUSync/Protocols/USyncDeviceProtocol.ts: -------------------------------------------------------------------------------- 1 | import { USyncQueryProtocol } from '../../Types/USync' 2 | import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild } from '../../WABinary' 3 | //import { USyncUser } from '../USyncUser' 4 | 5 | export type KeyIndexData = { 6 | timestamp: number 7 | signedKeyIndex?: Uint8Array 8 | expectedTimestamp?: number 9 | } 10 | 11 | export type DeviceListData = { 12 | id: number 13 | keyIndex?: number 14 | isHosted?: boolean 15 | } 16 | 17 | export type ParsedDeviceInfo = { 18 | deviceList?: DeviceListData[] 19 | keyIndex?: KeyIndexData 20 | } 21 | 22 | export class USyncDeviceProtocol implements USyncQueryProtocol { 23 | name = 'devices' 24 | 25 | getQueryElement(): BinaryNode { 26 | return { 27 | tag: 'devices', 28 | attrs: { 29 | version: '2', 30 | }, 31 | } 32 | } 33 | 34 | getUserElement(/* user: USyncUser */): BinaryNode | null { 35 | //TODO: Implement device phashing, ts and expectedTs 36 | //TODO: if all are not present, return null <- current behavior 37 | //TODO: otherwise return a node w tag 'devices' w those as attrs 38 | return null 39 | } 40 | 41 | parser(node: BinaryNode): ParsedDeviceInfo { 42 | const deviceList: DeviceListData[] = [] 43 | let keyIndex: KeyIndexData | undefined = undefined 44 | 45 | if(node.tag === 'devices') { 46 | assertNodeErrorFree(node) 47 | const deviceListNode = getBinaryNodeChild(node, 'device-list') 48 | const keyIndexNode = getBinaryNodeChild(node, 'key-index-list') 49 | 50 | if(Array.isArray(deviceListNode?.content)) { 51 | for(const { tag, attrs } of deviceListNode.content) { 52 | const id = +attrs.id 53 | const keyIndex = +attrs['key-index'] 54 | if(tag === 'device') { 55 | deviceList.push({ 56 | id, 57 | keyIndex, 58 | isHosted: !!(attrs['is_hosted'] && attrs['is_hosted'] === 'true') 59 | }) 60 | } 61 | } 62 | } 63 | 64 | if(keyIndexNode?.tag === 'key-index-list') { 65 | keyIndex = { 66 | timestamp: +keyIndexNode.attrs['ts'], 67 | signedKeyIndex: keyIndexNode?.content as Uint8Array, 68 | expectedTimestamp: keyIndexNode.attrs['expected_ts'] ? +keyIndexNode.attrs['expected_ts'] : undefined 69 | } 70 | } 71 | } 72 | 73 | return { 74 | deviceList, 75 | keyIndex 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/WAUSync/Protocols/USyncDisappearingModeProtocol.ts: -------------------------------------------------------------------------------- 1 | import { USyncQueryProtocol } from '../../Types/USync' 2 | import { assertNodeErrorFree, BinaryNode } from '../../WABinary' 3 | 4 | export type DisappearingModeData = { 5 | duration: number 6 | setAt?: Date 7 | } 8 | 9 | export class USyncDisappearingModeProtocol implements USyncQueryProtocol { 10 | name = 'disappearing_mode' 11 | 12 | getQueryElement(): BinaryNode { 13 | return { 14 | tag: 'disappearing_mode', 15 | attrs: {}, 16 | } 17 | } 18 | 19 | getUserElement(): null { 20 | return null 21 | } 22 | 23 | parser(node: BinaryNode): DisappearingModeData | undefined { 24 | if(node.tag === 'status') { 25 | assertNodeErrorFree(node) 26 | const duration: number = +node?.attrs.duration 27 | const setAt = new Date(+(node?.attrs.t || 0) * 1000) 28 | 29 | return { 30 | duration, 31 | setAt, 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/WAUSync/Protocols/USyncStatusProtocol.ts: -------------------------------------------------------------------------------- 1 | import { USyncQueryProtocol } from '../../Types/USync' 2 | import { assertNodeErrorFree, BinaryNode } from '../../WABinary' 3 | 4 | export type StatusData = { 5 | status?: string | null 6 | setAt?: Date 7 | } 8 | 9 | export class USyncStatusProtocol implements USyncQueryProtocol { 10 | name = 'status' 11 | 12 | getQueryElement(): BinaryNode { 13 | return { 14 | tag: 'status', 15 | attrs: {}, 16 | } 17 | } 18 | 19 | getUserElement(): null { 20 | return null 21 | } 22 | 23 | parser(node: BinaryNode): StatusData | undefined { 24 | if(node.tag === 'status') { 25 | assertNodeErrorFree(node) 26 | let status: string | null = node?.content?.toString() ?? null 27 | const setAt = new Date(+(node?.attrs.t || 0) * 1000) 28 | if(!status) { 29 | if(+node.attrs?.code === 401) { 30 | status = '' 31 | } else { 32 | status = null 33 | } 34 | } else if(typeof status === 'string' && status.length === 0) { 35 | status = null 36 | } 37 | 38 | return { 39 | status, 40 | setAt, 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/WAUSync/Protocols/UsyncBotProfileProtocol.ts: -------------------------------------------------------------------------------- 1 | import { USyncQueryProtocol } from '../../Types/USync' 2 | import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, getBinaryNodeChildString } from '../../WABinary' 3 | import { USyncUser } from '../USyncUser' 4 | 5 | export type BotProfileCommand = { 6 | name: string 7 | description: string 8 | } 9 | 10 | export type BotProfileInfo = { 11 | jid: string 12 | name: string 13 | attributes: string 14 | description: string 15 | category: string 16 | isDefault: boolean 17 | prompts: string[] 18 | personaId: string 19 | commands: BotProfileCommand[] 20 | commandsDescription: string 21 | } 22 | 23 | export class USyncBotProfileProtocol implements USyncQueryProtocol { 24 | name = 'bot' 25 | 26 | getQueryElement(): BinaryNode { 27 | return { 28 | tag: 'bot', 29 | attrs: { }, 30 | content: [{ tag: 'profile', attrs: { v: '1' } }] 31 | } 32 | } 33 | 34 | getUserElement(user: USyncUser): BinaryNode { 35 | return { 36 | tag: 'bot', 37 | attrs: { }, 38 | content: [{ tag: 'profile', attrs: { 'persona_id': user.personaId } }] 39 | } 40 | } 41 | 42 | parser(node: BinaryNode): BotProfileInfo { 43 | const botNode = getBinaryNodeChild(node, 'bot') 44 | const profile = getBinaryNodeChild(botNode, 'profile') 45 | 46 | const commandsNode = getBinaryNodeChild(profile, 'commands') 47 | const promptsNode = getBinaryNodeChild(profile, 'prompts') 48 | 49 | const commands: BotProfileCommand[] = [] 50 | const prompts: string[] = [] 51 | 52 | for(const command of getBinaryNodeChildren(commandsNode, 'command')) { 53 | commands.push({ 54 | name: getBinaryNodeChildString(command, 'name')!, 55 | description: getBinaryNodeChildString(command, 'description')! 56 | }) 57 | } 58 | 59 | for(const prompt of getBinaryNodeChildren(promptsNode, 'prompt')) { 60 | prompts.push(`${getBinaryNodeChildString(prompt, 'emoji')!} ${getBinaryNodeChildString(prompt, 'text')!}`) 61 | } 62 | 63 | 64 | return { 65 | isDefault: !!getBinaryNodeChild(profile, 'default'), 66 | jid: node.attrs.jid, 67 | name: getBinaryNodeChildString(profile, 'name')!, 68 | attributes: getBinaryNodeChildString(profile, 'attributes')!, 69 | description: getBinaryNodeChildString(profile, 'description')!, 70 | category: getBinaryNodeChildString(profile, 'category')!, 71 | personaId: profile!.attrs['persona_id'], 72 | commandsDescription: getBinaryNodeChildString(commandsNode, 'description')!, 73 | commands, 74 | prompts 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/WAUSync/Protocols/UsyncLIDProtocol.ts: -------------------------------------------------------------------------------- 1 | import { USyncQueryProtocol } from '../../Types/USync' 2 | import { BinaryNode } from '../../WABinary' 3 | 4 | export class USyncLIDProtocol implements USyncQueryProtocol { 5 | name = 'lid' 6 | 7 | getQueryElement(): BinaryNode { 8 | return { 9 | tag: 'lid', 10 | attrs: {}, 11 | } 12 | } 13 | 14 | getUserElement(): null { 15 | return null 16 | } 17 | 18 | parser(node: BinaryNode): string | null { 19 | if(node.tag === 'lid') { 20 | return node.attrs.val 21 | } 22 | 23 | return null 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/WAUSync/Protocols/index.ts: -------------------------------------------------------------------------------- 1 | export * from './USyncDeviceProtocol' 2 | export * from './USyncContactProtocol' 3 | export * from './USyncStatusProtocol' 4 | export * from './USyncDisappearingModeProtocol' -------------------------------------------------------------------------------- /src/WAUSync/USyncQuery.ts: -------------------------------------------------------------------------------- 1 | import { USyncQueryProtocol } from '../Types/USync' 2 | import { BinaryNode, getBinaryNodeChild } from '../WABinary' 3 | import { USyncBotProfileProtocol } from './Protocols/UsyncBotProfileProtocol' 4 | import { USyncLIDProtocol } from './Protocols/UsyncLIDProtocol' 5 | import { USyncContactProtocol, USyncDeviceProtocol, USyncDisappearingModeProtocol, USyncStatusProtocol } from './Protocols' 6 | import { USyncUser } from './USyncUser' 7 | 8 | export type USyncQueryResultList = { [protocol: string]: unknown, id: string } 9 | 10 | export type USyncQueryResult = { 11 | list: USyncQueryResultList[] 12 | sideList: USyncQueryResultList[] 13 | } 14 | 15 | export class USyncQuery { 16 | protocols: USyncQueryProtocol[] 17 | users: USyncUser[] 18 | context: string 19 | mode: string 20 | 21 | constructor() { 22 | this.protocols = [] 23 | this.users = [] 24 | this.context = 'interactive' 25 | this.mode = 'query' 26 | } 27 | 28 | withMode(mode: string) { 29 | this.mode = mode 30 | return this 31 | } 32 | 33 | withContext(context: string) { 34 | this.context = context 35 | return this 36 | } 37 | 38 | withUser(user: USyncUser) { 39 | this.users.push(user) 40 | return this 41 | } 42 | 43 | parseUSyncQueryResult(result: BinaryNode): USyncQueryResult | undefined { 44 | if(result.attrs.type !== 'result') { 45 | return 46 | } 47 | 48 | const protocolMap = Object.fromEntries(this.protocols.map((protocol) => { 49 | return [protocol.name, protocol.parser] 50 | })) 51 | 52 | const queryResult: USyncQueryResult = { 53 | // TODO: implement errors etc. 54 | list: [], 55 | sideList: [], 56 | } 57 | 58 | const usyncNode = getBinaryNodeChild(result, 'usync') 59 | 60 | //TODO: implement error backoff, refresh etc. 61 | //TODO: see if there are any errors in the result node 62 | //const resultNode = getBinaryNodeChild(usyncNode, 'result') 63 | 64 | const listNode = getBinaryNodeChild(usyncNode, 'list') 65 | if(Array.isArray(listNode?.content) && typeof listNode !== 'undefined') { 66 | queryResult.list = listNode.content.map((node) => { 67 | const id = node?.attrs.jid 68 | const data = Array.isArray(node?.content) ? Object.fromEntries(node.content.map((content) => { 69 | const protocol = content.tag 70 | const parser = protocolMap[protocol] 71 | if(parser) { 72 | return [protocol, parser(content)] 73 | } else { 74 | return [protocol, null] 75 | } 76 | }).filter(([, b]) => b !== null) as [string, unknown][]) : {} 77 | return { ...data, id } 78 | }) 79 | } 80 | 81 | //TODO: implement side list 82 | //const sideListNode = getBinaryNodeChild(usyncNode, 'side_list') 83 | return queryResult 84 | } 85 | 86 | withDeviceProtocol() { 87 | this.protocols.push(new USyncDeviceProtocol()) 88 | return this 89 | } 90 | 91 | withContactProtocol() { 92 | this.protocols.push(new USyncContactProtocol()) 93 | return this 94 | } 95 | 96 | withStatusProtocol() { 97 | this.protocols.push(new USyncStatusProtocol()) 98 | return this 99 | } 100 | 101 | withDisappearingModeProtocol() { 102 | this.protocols.push(new USyncDisappearingModeProtocol()) 103 | return this 104 | } 105 | 106 | withBotProfileProtocol() { 107 | this.protocols.push(new USyncBotProfileProtocol()) 108 | return this 109 | } 110 | 111 | withLIDProtocol() { 112 | this.protocols.push(new USyncLIDProtocol()) 113 | return this 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/WAUSync/USyncUser.ts: -------------------------------------------------------------------------------- 1 | export class USyncUser { 2 | id: string 3 | lid: string 4 | phone: string 5 | type: string 6 | personaId: string 7 | 8 | withId(id: string) { 9 | this.id = id 10 | return this 11 | } 12 | 13 | withLid(lid: string) { 14 | this.lid = lid 15 | return this 16 | } 17 | 18 | withPhone(phone: string) { 19 | this.phone = phone 20 | return this 21 | } 22 | 23 | withType(type: string) { 24 | this.type = type 25 | return this 26 | } 27 | 28 | withPersonaId(personaId: string) { 29 | this.personaId = personaId 30 | return this 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/WAUSync/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Protocols' 2 | export * from './USyncQuery' 3 | export * from './USyncUser' -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import makeWASocket from './Socket' 2 | 3 | export * from '../WAProto' 4 | export * from './Utils' 5 | export * from './Types' 6 | export * from './Defaults' 7 | export * from './WABinary' 8 | export * from './WAM' 9 | export * from './WAUSync' 10 | 11 | export type WASocket = ReturnType 12 | export { makeWASocket } 13 | export default makeWASocket 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "CommonJS", 5 | "experimentalDecorators": true, 6 | "allowJs": false, 7 | "checkJs": false, 8 | "outDir": "lib", 9 | "strict": false, 10 | "strictNullChecks": true, 11 | "skipLibCheck": true, 12 | "noImplicitThis": true, 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "declaration": true, 17 | "lib": ["es2020", "esnext.array", "DOM"] 18 | }, 19 | "include": ["./src/**/*.ts"], 20 | "exclude": ["node_modules", "src/Tests/*", "src/Binary/GenerateStatics.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/index.ts"], 3 | "out": "docs/api", 4 | "plugin": ["typedoc-plugin-markdown"], 5 | "excludePrivate": true, 6 | "excludeProtected": true, 7 | "excludeExternals": true, 8 | "includeVersion": false, 9 | "hideBreadcrumbs": true, 10 | "hidePageHeader": true, 11 | "entryFileName": "index.md", 12 | "readme": "none" 13 | } 14 | --------------------------------------------------------------------------------