├── .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 |

2 |
3 | 
4 | 
5 | 
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)}${node.tag}>` : '/>'
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 |
--------------------------------------------------------------------------------