chore: cleanup, ci workflows and formatting (#127064)

* ci: setup workflows, dependabot, labeler and commit linting

* docs: update README, community files and issue templates

* chore: configure prettier, editorconfig and format codebase

* chore: restore changes lost during rebase

* docs: adjust url for new logo

* fix: align badges

* chore: resolve conflicts and re-add temp lint
This commit is contained in:
2^1 2026-03-10 00:45:12 +01:00 • committed by GitHub
commit 807efd502d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
94 changed files with 6790 additions and 3547 deletions

View file

@ -4,5 +4,9 @@ root = true
indent_style = space
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
[*.md]
trim_trailing_whitespace = false
insert_final_newline = false

View file

@ -1,3 +1,2 @@
WWEBJS_TEST_REMOTE_ID=XXXXXXXXXX@c.us
WWEBJS_TEST_CLIENT_ID=authenticated
WWEBJS_TEST_MD=1

8
.eslintignore Normal file
View file

@ -0,0 +1,8 @@
node_modules
dist
coverage
docs
*.min.js
.wa-version
.wwebjs_auth
.wwebjs_cache

View file

@ -5,7 +5,11 @@
"es6": true,
"node": true
},
"extends": ["eslint:recommended", "plugin:mocha/recommended"],
"extends": [
"eslint:recommended",
"plugin:mocha/recommended",
"plugin:prettier/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
@ -13,24 +17,5 @@
"parserOptions": {
"ecmaVersion": 2022
},
"plugins": ["mocha"],
"ignorePatterns": ["docs"],
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
"plugins": ["mocha"]
}

4
.gitattributes vendored Normal file
View file

@ -0,0 +1,4 @@
* text=auto eol=lf
package-lock.json linguist-generated=true text=auto eol=lf
docs/* linguist-generated=true text=auto eol=lf

View file

@ -1,4 +1,3 @@
# Contributor Covenant Code of Conduct
## Our Pledge
@ -18,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@ -61,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[pedroslopez@me.com](mailto:pedroslopez@me.com).
[pedroslopez@me.com][email].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
@ -107,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
@ -126,6 +125,7 @@ For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[email]: mailto:pedroslopez@me.com
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity

204
.github/COMMIT_CONVENTION.md vendored Normal file
View file

@ -0,0 +1,204 @@
# Git Commit Message Convention
> This is adapted from [Angular's commit convention][angular-commit-convention].
Messages must be matched by the following regex (case-insensitive):
```ts
/^(revert: )?(feat|fix|docs|style|refactor|perf|test|build|ci|chore|types|infra)(\(.+\))?!?: .{1,72}/i;
```
The commit type is **case-insensitive** — `feat`, `Feat`, and `FEAT` are all valid.
## Examples
### Features
Appears under "Features" header, message subheader:
```txt
feat(message): add 'downloadMedia' option
```
Additional examples:
```txt
feat(chat): add ability to pin messages
```
```txt
feat(contact): add getProfilePicUrl method
```
### Bug Fixes
Appears under "Bug Fixes" header, client subheader, with a link to issue #28:
```txt
fix(client): stop client breaking when QR code expires
Closes #28
```
Additional examples:
```txt
fix(message): handle media messages without caption correctly
```
```txt
fix(auth): resolve authentication timeout on slow connections
```
### Performance Improvements
Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation:
```txt
perf(message): remove legacyMode option
BREAKING CHANGE: The legacyMode option has been removed. The new message handling is always used for performance reasons.
```
Additional example:
```txt
perf(client): optimize event listener registration
```
### Documentation
```txt
docs(readme): update installation instructions
```
```txt
docs(api): add examples for message reactions
```
### Code Refactoring
```txt
refactor(client): simplify authentication flow
```
```txt
refactor(structures): extract common message properties
```
### Tests
```txt
test(message): add unit tests for media download
```
```txt
test(client): add integration tests for reconnection
```
### Build System
```txt
build(deps): update puppeteer to v21.0.0
```
```txt
build(webpack): optimize bundle size
```
### Continuous Integration
```txt
ci(actions): add automated release workflow
```
```txt
ci(tests): run tests on multiple node versions
```
### Chores
```txt
chore(gitignore): ignore .vscode directory
```
```txt
chore(deps): bump dependencies to latest versions
```
### Type Definitions
```txt
types(message): add missing type for MessageMedia
```
```txt
types(client): improve ClientOptions interface
```
### Code Style
```txt
style(client): fix indentation and spacing
```
### Reverts
The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header:
```txt
revert: feat(message): add 'downloadMedia' option
This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
```
A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
```txt
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
The **header** is mandatory and the **scope** of the header is optional.
## Revert
If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body, it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted.
## Type
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However if there is any [BREAKING CHANGE](#footer), the commit will always appear in the changelog.
Other prefixes are up to your discretion. Suggested prefixes are `build`, `ci`, `docs`, `style`, `refactor`, and `test` for non-changelog related tasks.
## Scope
The scope could be anything specifying place of the commit change. For example `client`, `message`, `chat`, `contact`, `media`, `auth`, etc...
## Subject
The subject contains succinct description of the change:
- use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize first letter
- no dot (.) at the end
## Body
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
The body should include the motivation for the change and contrast this with previous behavior.
## Footer
The footer should contain any information about **Breaking Changes** and is also the place to
reference GitHub issues that this commit **Closes**.
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
A detailed explanation can be found in this [document][commit-message-format].
[angular-commit-convention]: https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular
[commit-message-format]: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#

43
.github/CONTRIBUTING.md vendored Normal file
View file

@ -0,0 +1,43 @@
# Contributing
If you want to contribute to whatsapp-web.js, start by forking the repository and opening a pull request with your changes. The project uses ESLint and Prettier to enforce a consistent coding style, so setting them up in your development environment is recommended to ensure your contributions meet the formatting standards.
## Questions
Do not open issues for general questions or support requests. For help, please join the [Discord Server][discord] instead. Installation instructions and general guidance are available in the documentation. Issues opened for questions will be closed.
### Issue Tracker
Before creating a new issue, review the existing [issue tracker][issue-tracker] to avoid duplicates.
## Issues and Pull Requests
- **Bug reports** must include a minimal reproduction and clear steps to reproduce the problem.
- **Feature requests** should clearly describe the use case and explain why the feature belongs in the library.
- **Pull requests and commit messages** must follow the conventional commit format for both commit messages and the PR title (e.g. `feat(client): add sendMessage option`). The commit type is case-insensitive (`feat`, `Feat`, and `FEAT` are all valid).
- All pull requests should target the `main` branch unless discussed otherwise.
- Ensure that linting and tests pass before submitting your pull request.
## Setup
To prepare your development environment:
1. Fork and clone the repository, and ensure you are on the `main` branch.
2. Run `npm install` to install all dependencies.
3. Implement your changes or improvements.
4. Run `npm run check` to execute ESLint and Prettier checks.
5. Run `npm test` to ensure the test suite passes.
6. Submit a pull request (make sure it follows the conventional commit format).
## Testing Locally
To test your changes within your own project:
1. Clone the repository and apply your modifications.
2. Run `npm install` inside the cloned directory.
3. In your project, run `npm link <path-to-your-clone>` to create a symlink to your local version.
4. Import the package and verify your changes.
5. If everything works as expected, you’ve successfully implemented your changes without breaking the library.
[discord]: https://discord.wwebjs.dev/
[issue-tracker]: https://github.com/pedroslopez/whatsapp-web.js/issues

9
.github/DISCLAIMER.md vendored Normal file
View file

@ -0,0 +1,9 @@
# Disclaimer
This project is an unofficial library and is not affiliated with, endorsed by, maintained, authorized, or sponsored by WhatsApp Inc., Meta Platforms, or any of their subsidiaries or affiliates. Use of this library and any related components is entirely at your own risk. WhatsApp prohibits the use of unofficial clients, reverse-engineered integrations, automated bots, or other non-official methods of access to its platform, and using this library may violate WhatsApp’s Terms of Service, potentially resulting in temporary or permanent suspension of your WhatsApp account or other enforcement actions. We assume no responsibility or liability if your account is restricted, suspended, or permanently banned, and you are solely responsible for any consequences arising from the use or misuse of this project.
The maintainers and contributors provide this library without any guarantees or warranties, express or implied, regarding its reliability, functionality, or long-term viability. Compatibility with WhatsApp’s private or internal APIs may change at any time without notice, which could cause the library to stop functioning or behave unpredictably. This project has been developed strictly for educational, research, experimental, and hobby purposes and should not be used in commercial, production, or business-critical environments without fully understanding and accepting the associated risks.
For official, secure, and policy-compliant integrations, individuals and businesses are strongly encouraged to use the WhatsApp Business Platform or other authorized APIs provided directly by WhatsApp or Meta. The official WhatsApp website is available at [https://www.whatsapp.com][whatsapp]. "WhatsApp" and all related names, marks, logos, and images are registered trademarks of their respective owners. By using this project, you acknowledge that you understand and accept all of the above terms, limitations, and potential risks, including the possibility of account suspension or banning.
[whatsapp]: https://www.whatsapp.com

2
.github/FUNDING.yml vendored
View file

@ -1,3 +1,3 @@
github: [pedroslopez, PurpShell]
ko_fi: pedroslopez
custom: ["paypal.me/psla", "buymeacoff.ee/pedroslopez"]
custom: ['paypal.me/psla', 'buymeacoff.ee/pedroslopez']

164
.github/ISSUE_TEMPLATE/01-bug_report.yml vendored Normal file
View file

@ -0,0 +1,164 @@
name: Bug Report
description: Report a bug in the library or another package
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug! Please check the [issue tracker](https://github.com/wwebjs/wwebjs/issues) before submitting.
> [!CAUTION]
> - **Report only issues related to the `whatsapp-web.js` library!**
> - **Any issue without a clear reproduction will be closed!**
> - **Any issue opened as a question will be closed!**
> - **Duplicate issues will be closed.**
- type: dropdown
id: priority
attributes:
label: Issue Priority
description: Be realistic. Elaborate in the description if needed.
options:
- Low (slightly annoying)
- Medium (should be fixed soon)
- High (immediate attention needed)
default: 0
validations:
required: true
- type: textarea
id: description
attributes:
label: Issue Description
description: Provide a clear and concise description of the problem.
placeholder: |
When I try to send a message, the client disconnects unexpectedly...
validations:
required: true
- type: textarea
id: reproduction_steps
attributes:
label: Steps To Reproduce
description: How can we reproduce this issue?
placeholder: |
1. Do this
2. Do that
...
9. See error
validations:
required: true
- type: textarea
id: code_sample
attributes:
label: Code Sample
description: |
Provide code that reproduces the bug. Ensure it is:
- **Minimal** - Only essential code to show the issue
- **Complete** - Include all dependencies and setup
- **Tested** - Verify it actually triggers the problem
Will be auto-formatted - no backticks needed.
render: typescript
validations:
required: false
- type: dropdown
id: whatsapp_type
attributes:
label: WhatsApp Account Type
description: What type of WhatsApp account are you using?
options:
- Standard
- WhatsApp Business
validations:
required: true
- type: dropdown
id: auth_type
attributes:
label: Authentication Strategy
description: What authenctication strategy are you using?
options:
- LocalAuth
- RemoteAuth
- NoAuth
validations:
required: true
- type: input
id: lib_version
attributes:
label: whatsapp-web.js Version
description: What library version are you using? Check it in `package.json` file.
placeholder: 1.31.0
validations:
required: true
- type: input
id: wweb_version
attributes:
label: WhatsApp Web Version
description: 'What WhatsApp Web version are you using? Use: `await client.getWWebVersion()`'
placeholder: 2.3000.1017054665
validations:
required: true
- type: input
id: browser_type
attributes:
label: Browser Type
description: What web browser are you using?
placeholder: Chromium / Google Chrome / Edge / ...
validations:
required: true
- type: input
id: browser_version
attributes:
label: Browser Version
description: What version of the browser are you using?
placeholder: 120.0.6099.71
validations:
required: true
- type: input
id: computer_os
attributes:
label: Computer OS Version
description: What OS are you using?
placeholder: MacOS 15 / Windows 11 / Ubuntu 24 / ...
validations:
required: true
- type: input
id: phone_os
attributes:
label: Phone OS Version
description: What OS version are you using on your phone?
placeholder: Android 12 / iOS 15.4 / ...
validations:
required: true
- type: input
id: node_version
attributes:
label: Node.js Version
description: What Node.js version are you using? Run `node -v` in your terminal.
placeholder: 18.16.0
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: '**I use the latest released version of whatsapp-web.js.**'
required: true
- label: I have searched existing issues and confirmed this is not a duplicate.
required: true
- label: I have verified this is a library issue, not a problem with my code.
required: true

View file

@ -0,0 +1,55 @@
name: Feature Request
description: Request a new feature for our applications or packages
labels: [feature request]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest a feature! Please search the [issue tracker](https://github.com/wwebjs/wwebjs/issues) first to avoid duplicates.
> [!CAUTION]
> - **Please submit only feature requests that related to the `whatsapp-web.js` library.**
> - **Any feature request opened as a question will be closed!**
> - **Duplicate issues will be closed.**
- type: textarea
id: description
attributes:
label: Feature summary
description: Briefly describe the problem/use case and desired capability.
placeholder: |
Problem: ...
Benefit: ...
Outcome: ...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Ideal solution or implementation
description: How should this feature work? Include examples like API shape, UX sketches.
placeholder: |
API: client.xyz(...)
Event: client.on('xyz', ...)
Example: const r = await client.xyz()
Compatibility: Breaking changes?
render: markdown
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives / Additional context
description: Alternatives or workarounds? / More notes/links/screenshots.
- type: checkboxes
id: checklist
attributes:
label: Checklist
options:
- label: I have searched existing issues and confirmed this is not a duplicate.
required: true
- label: This request is within the scope of WhatsApp Web capabilities.
required: false

View file

@ -1,140 +0,0 @@
name: '🐛 Bug Report'
description: Create a report to help us improve
labels: bug
body:
- type: markdown
attributes:
value: |
### Thank you for reporting an issue.
### Here are some important points to consider before submitting a new bug or issue:
- Please report only issues related to the `whatsapp-web.js` library.
- Duplicate issues will be closed.
- Any issue opened as a question will be closed.
### If you have any question about how to achieve something and are struggling, please post a question in our [Discord Server](https://discord.gg/wyKybbF) instead.
### Please check the links below to see if you can find a solution there:
- [Open Issues](https://github.com/pedroslopez/whatsapp-web.js/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc)
- [Closed Issues](https://github.com/pedroslopez/whatsapp-web.js/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed)
- [Library Documentation](https://docs.wwebjs.dev/)
- [Guide](https://wwebjs.dev/guide)
- [JavaScript Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide)
- [Node.js Documentation](https://nodejs.org/en/docs/)
### The more information you fill in, the better the community can help you.
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues.
required: true
- type: checkboxes
attributes:
label: Is this a problem caused by your code, or is it specifically because of the library?
description: Please check your code once again to make sure it is correct.
options:
- label: I have double-checked my code carefully.
required: true
- type: textarea
id: description
attributes:
label: Describe the bug.
description: Provide a clear and concise description of the challenge you are running into.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: Provide a clear and concise description of what you expected to happen.
placeholder: |
As a user, I expected ___ behavior, but I am seeing ___
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to Reproduce the Bug or Issue
description: Describe the steps we have to take to reproduce the behavior. Additionally, **please include the code snippet you used.** Use [syntax highlighting](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) for more readability.
placeholder: |
Your steps to reproduce the bug here...
```js
// your code here
```
validations:
required: true
- type: dropdown
id: whatsapp_type
attributes:
label: WhatsApp Account Type
options:
- Standard
- WhatsApp Business
validations:
required: true
- type: input
id: browser_type
attributes:
label: Browser Type
description: What web browser are you using? **Also provide the browser version.**
placeholder: Chromium | Google Chrome | other (provide the type)
validations:
required: true
- type: input
id: os
attributes:
label: Operation System Type
description: What OS are you using? **Also provide its version.**
placeholder: Mac | Windows | Linux | Docker + Ubuntu | other (provide the type)
validations:
required: true
- type: input
id: phone_os
attributes:
label: Phone OS Type
description: What OS are you using on your phone? **Also provide its version.**
placeholder: Android | iOS | other (provide the type)
validations:
required: true
- type: input
id: lib_version
attributes:
label: WhatsApp-Web.js Version
description: What library version are you using? Check it in `package.json` file.
validations:
required: true
- type: input
id: wweb_version
attributes:
label: WhatsApp Web Version
description: What WhatsApp Web version are you using? Run `await client.getWWebVersion()`.
validations:
required: true
- type: input
id: node_version
attributes:
label: Node.js Version
description: What Node.js version are you using? Run `node -v` in your terminal.
validations:
required: true
- type: dropdown
id: auth_type
attributes:
label: Authentication Strategy
description: What authenctication strategy are you using?
options:
- LocalAuth
- RemoteAuth
- NoAuth
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional Context
description: Add any other context about the problem here.

View file

@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Official WWebJS Discord Server
url: https://discord.gg/H7DqQs4
about: Please ask and answer questions here.
- name: Discord Server
url: https://discord.wwebjs.dev
about: Please ask and answer questions here.

View file

@ -1,71 +0,0 @@
name: 🚀 Feature Request
description: Suggest an idea for this project
labels: enhancement
body:
- type: markdown
attributes:
value: |
### Thank you for suggesting a new idea for this project.
### Here are some important points to consider before continuing:
- Please submit only feature requests that related to the `whatsapp-web.js` library.
- Duplicate feature requests will be closed.
- Any feature request opened as a question will be closed.
### If you have any question about how to achieve something and are struggling, please post a question in our [Discord Server](https://discord.gg/wyKybbF) instead.
### Please check the links below to see if you can find a solution there:
- [Open Pull Requests](https://github.com/pedroslopez/whatsapp-web.js/pulls?q=is%3Apr+is%3Aopen+-author%3Aapp%2Fdependabot)
- [Merged Pull Requests](https://github.com/pedroslopez/whatsapp-web.js/pulls?q=is%3Apr+is%3Amerged+-author%3Aapp%2Fdependabot)
- [Library Documentation](https://docs.wwebjs.dev/)
- [Guide](https://wwebjs.dev/guide)
- [JavaScript Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide)
- [Node.js Documentation](https://nodejs.org/en/docs/)
- type: checkboxes
attributes:
label: Is the feature you are looking for already implemented in the library?
description: Please check to see if an issue related to this feature request has [already been implemented](https://github.com/pedroslopez/whatsapp-web.js/pulls?q=is%3Apr+is%3Amerged+-author%3Aapp%2Fdependabot).
options:
- label: I have reviewed the implemented features.
required: true
- type: checkboxes
attributes:
label: Is your "solution" already implemented in an opened pull request?
description: Please check the [open pull requests](https://github.com/pedroslopez/whatsapp-web.js/pulls?q=is%3Apr+is%3Aopen+-author%3Aapp%2Fdependabot) to see if there is already an issue related to this feature request.
options:
- label: I have reviewed the existing pull requests.
required: true
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.
description: A concise description of the problem you are facing or the motivation behind this feature request.
placeholder: I faced a problem due to...
validations:
required: false
- type: textarea
attributes:
label: Describe the solution you'd like.
description: A concise description of the solution for the issue.
validations:
required: true
- type: textarea
attributes:
label: Describe an alternate solution if you have one.
description: Is there any other approach to solve the problem?
validations:
required: false
- type: textarea
attributes:
label: Additional Context
description: |
Links? Screenshots? References? Anything that will give us more context about what you would like to see!
validations:
required: false

10
.github/SUPPORT.md vendored Normal file
View file

@ -0,0 +1,10 @@
# Need Help?
This issue tracker is **only** for bug reports and feature requests.
For questions and general help, please join our **Discord server**: [https://discord.wwebjs.dev][discord].
Installation instructions and guidance can be found in the documentation.
Issues that are not bug reports, feature requests or questions will be closed.
[discord]: https://discord.wwebjs.dev

View file

@ -1,7 +1,11 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
open-pull-requests-limit: 10
schedule:
interval: "monthly"
- package-ecosystem: 'npm'
directory: '/'
open-pull-requests-limit: 10
schedule:
interval: 'monthly'
labels:
- 'dependencies'
- 'dependabot'
target-branch: 'main'

24
.github/issue-labeler.yml vendored Normal file
View file

@ -0,0 +1,24 @@
documentation:
- "\\b(doc|docs|readme|documentation|typo|example|guide)s?\\b"
security:
- "\\b(vulnerability|xss|csrf|security|exploit|attack|inject)s?\\b"
performance:
- "\\b(performance|slow|memory|cpu|optimize|optimization|lag|leak)s?\\b"
breaking change:
- "\\bbreaking.?changes?\\b"
dependencies:
- "\\b(depend(enc)?y|dependencies|deps?|package|upgrade|bump)s?\\b"
typings:
- "\\b(type|types|typing|typings|typescript|\\.d\\.ts)s?\\b"
tests:
- "\\b(test|tests|testing|spec|mocha|assert|coverage)s?\\b"
ci:
- "\\b(ci|workflow|pipeline|action|github.?action|continuous.?integration)s?\\b"
build:
- "\\b(build|builds|build.?system|webpack|rollup|esbuild|gulp|grunt)s?\\b"
infra:
- "\\b(infra|infrastructure|devops|docker|kubernetes|cloud|aws|azure|gcp)s?\\b"
styles:
- "\\b(style|styles|format|lint|prettier|eslint)s?\\b"
ui:
- "\\b(ui|ux|interface|design|layout|responsive)s?\\b"

148
.github/labels.yml vendored Normal file
View file

@ -0,0 +1,148 @@
# Issue Labels
- name: 'bug'
color: '#d73a4a'
description: 'Something is broken'
- name: 'feature request'
color: '#a2eeef'
description: 'Request for a new feature'
# Status Labels
- name: 'in progress'
color: '#fbca04'
description: 'Work in progress'
- name: 'in review'
color: '#0e8a16'
description: 'Awaiting review'
- name: 'merge ready'
color: '#0e8a16'
description: 'Ready to merge'
- name: 'blocked'
color: '#d13917'
description: 'Blocked by another issue'
- name: 'discussion'
color: '#d876e3'
description: 'Needs discussion'
- name: 'approved'
color: '#11EA91'
description: 'Confirmed by maintainers'
- name: 'stale'
color: '#bfd4f2'
description: 'Inactive for a while'
- name: 'waiting for a response'
color: '#BE4422'
description: 'Waiting for author response'
- name: 'waiting for testers'
color: '#7d006a'
description: 'Needs testing'
# Priority & Help
- name: 'good first issue'
color: '#7057ff'
description: 'Beginner-friendly issue'
- name: 'help wanted'
color: '#008672'
description: 'Community help needed'
- name: 'high priority'
color: '#00DFA8'
description: 'Important issue'
# Bug Issues
- name: 'issued low'
color: '#d4c5f9'
description: 'Low impact issue'
- name: 'issued medium'
color: '#d4c5f9'
description: 'Medium impact issue'
- name: 'issued high'
color: '#d4c5f9'
description: 'High impact issue'
# Resolution
- name: 'duplicate'
color: '#cfd3d7'
description: 'Already reported'
- name: 'invalid'
color: '#e4e669'
description: 'Not a valid issue'
- name: 'wontfix'
color: '#ffffff'
description: 'Will not be fixed'
# Type of Change
- name: 'api changes'
color: '#5319e7'
description: 'API modifications'
- name: 'dependencies'
color: '#0366d6'
description: 'Dependency updates'
- name: 'security'
color: '#ee0701'
description: 'Security related'
- name: 'performance'
color: '#ff6b6b'
description: 'Performance improvements'
- name: 'error handling'
color: '#f9d0c4'
description: 'Error handling updates'
- name: 'typings'
color: '#1d76db'
description: 'Type definitions'
- name: 'interactions'
color: '#c5def5'
description: 'Interaction changes'
- name: 'breaking change'
color: '#E9314A'
description: 'Breaks existing behavior'
# Bots & Automation
- name: 'dependabot'
color: '#0366d6'
description: 'Dependabot PR'
- name: 'depfu'
color: '#0000ff'
description: 'Depfu PR'
# Code & Build
- name: 'tests'
color: '#bfd4f2'
description: 'Test related'
- name: 'build'
color: '#0075ca'
description: 'Build system'
- name: 'ci'
color: '#0075ca'
description: 'CI pipeline'
- name: 'infra'
color: '#d4c5f9'
description: 'Infrastructure'
- name: 'styles'
color: '#ffc0cb'
description: 'Code style'
- name: 'ui'
color: '#ffaad4'
description: 'UI changes'
- name: 'utility'
color: '#ededed'
description: 'Utility code'
- name: 'chore'
color: '#fef2c0'
description: 'Maintenance task'
# Quality & Review
- name: 'AI pr'
color: '#b60205'
description: 'AI-assisted PR; Review very carefully!'
- name: 'bad pr'
color: '#B60205'
description: 'Below project standards'
# Project Specific
- name: 'docs'
color: '#0075ca'
description: 'Documentation changes'
- name: 'whatsapp bug'
color: '#1d76db'
description: 'WhatsApp Web bug'
- name: 'whatsapp change'
color: '#25D366'
description: 'WhatsApp Web change'

65
.github/pr-labeler.yml vendored Normal file
View file

@ -0,0 +1,65 @@
api changes:
- changed-files:
- any-glob-to-any-file:
- src/*
- src/**/*
dependencies:
- changed-files:
- any-glob-to-any-file:
- package.json
- package-lock.json
tests:
- changed-files:
- any-glob-to-any-file:
- tests/*
- tests/**/*
docs:
- changed-files:
- any-glob-to-any-file:
- docs/*
- docs/**/*
- '**/*.md'
- .jsdoc.json
infra:
- changed-files:
- any-glob-to-any-file:
- .github/*
- .github/**/*
ci:
- changed-files:
- any-glob-to-any-file:
- .github/workflows/*
- .github/workflows/**/*
typings:
- changed-files:
- any-glob-to-any-file:
- index.d.ts
- '**/*.d.ts'
utility:
- changed-files:
- any-glob-to-any-file:
- src/util/*
- src/util/**/*
build:
- changed-files:
- any-glob-to-any-file:
- tools/*
- tools/**/*
- .eslintrc.json
- .eslintignore
- .prettierrc.json
- .prettierignore
- .editorconfig
- .gitignore
- .gitattributes
- .commitlint.config.js
chore:
- changed-files:
- any-glob-to-any-file:
- .env.example
- .npmignore
- LICENSE
- .github/FUNDING.yml
- .github/CODE_OF_CONDUCT.md
- .github/ISSUE_TEMPLATE/*
- .github/pull_request_template.md

View file

@ -1,53 +1,79 @@
# PR Details
<!-- Provide here a general summary of your changes. -->
## Description
<!-- Describe here your changes in detail. -->
<!-- What does this PR change and why? -->
## Related Issue(s)
<!-- Optional --->
<!-- If there is an issue related to the PR, link it here using a 'closes' keyword, for example: -->
<!-- closes #XXXX (where the XXXX is an issue number) -->
<!-- If there are multiple issues, link them as follows: -->
<!-- closes #XXXX closes #YYYY closes #ZZZZ -->
<!-- See more here: https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword -->
<!--
If this pull request relates to one or more GitHub issues, link them using GitHub's closing keywords to automatically close the issues upon merging.
## Motivation and Context
Examples:
closes #123
fixes #456
resolves #789
<!-- Optional --->
<!-- Why is this change required? What problem does it solve? -->
For more details, see:
https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue
-->
## How Has This Been Tested
## Testing Summary
<!-- Please describe in detail how you tested your changes. -->
### Test Details
<!-- Describe in detail how you tested your changes. -->
### Environment
<!-- Include details of your testing environment: -->
- Machine OS: <!-- The operation system of a machine you tested the PR on. (Mac | Windows | Linux | Docker + Ubuntu | other (provide the type)) -->
- Phone OS: <!-- The operation system of a phone you used to check the the PR functionality on. -->
- Library Version: <!-- The whatsapp-web.js version you used to test the PR. -->
- WhatsApp Web Version: <!-- Run `await client.getWWebVersion()` to see the WWeb version you used to test the PR. -->
- Puppeteer Version:
- Browser Type and Version: <!-- Chromium XX | Google Chrome XX | other (provide the type and version) -->
- Node Version: <!-- Run `npm -v` in your terminal to see the version of Node.js being used. -->
<!-- Provide detailed information about the environment used to test this pull request. Simply fill out the blanks with your setup. -->
## Types of changes
<!-- Operating system of the machine used for testing -->
<!-- Example: (macOS 15 / Windows 11 / Ubuntu 24 / ...) -->
<!-- What types of changes does your code introduce? Put an `X` in all the boxes that apply: -->
- Machine OS:
- [ ] Dependency change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
<!-- Operating system of the mobile device used to verify functionality -->
<!-- Example: (Android 12 / iOS 16 / ...) -->
- Phone OS:
<!-- Version of whatsapp-web.js used during testing -->
<!-- Check your `package.json` file -->
<!-- Example: (1.31.0) -->
- Library Version:
<!-- WhatsApp Web version used for testing -->
<!-- Run `await client.getWWebVersion()` to retrieve it -->
<!-- Example: (2.3000.1017054665) -->
- WhatsApp Web Version:
<!-- Browser and version used for testing -->
<!-- Example: (Chromium 120 / Google Chrome 118 / ...) -->
- Browser Type and Version:
<!-- Node.js version used during testing -->
<!-- Run `node -v` to check -->
<!-- Example: (18.16.0) -->
- Node Version:
## Type of Change
<!-- Select all that apply: -->
- [ ] **Dependency change** _(package changes such as removals, upgrades, or additions)_
- [ ] **Bug fix** _(non-breaking change that fixes an issue)_
- [ ] **New feature** _(non-breaking change that adds functionality)_
- [ ] **Breaking change** _(fix or feature that would cause existing functionality to change)_
- [ ] **Non-code change** _(documentation, README, etc.)_
## Checklist
<!-- Go over all the following points, and put an `X` in all the boxes that apply: -->
<!-- Please confirm that all relevant items below have been completed. -->
- [ ] My code follows the code style of this project.
- [ ] I have updated the documentation accordingly (index.d.ts).
- [ ] I have updated the usage example accordingly (example.js)
- [ ] My code follows the style guidelines of this project.
- [ ] All new and existing tests pass (`npm test`).
- [ ] Typings (e.g. `index.d.ts`) have been updated if necessary.
- [ ] Usage examples (e.g. `example.js`) / documentation have been updated if applicable.

20
.github/stale.yml vendored
View file

@ -1,20 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 30
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- enhancement
- bug
- WhatsApp Change
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

112
.github/workflows/format-issue-bug.yml vendored Normal file
View file

@ -0,0 +1,112 @@
name: Format Issue Body
on:
issues:
types: [opened]
permissions:
contents: read
issues: write
concurrency:
group: issue-format-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
format:
runs-on: ubuntu-latest
if: github.event_name == 'issues' && contains(github.event.issue.labels.*.name, 'bug')
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Parse issue form body
uses: stefanbuck/github-issue-parser@v3
id: parse
with:
template-path: .github/ISSUE_TEMPLATE/01-bug_report.yml
- name: Build formatted issue body
id: summary
env:
DESCRIPTION: ${{ steps.parse.outputs.issueparser_description }}
REPRODUCTION_STEPS: ${{ steps.parse.outputs.issueparser_reproduction_steps }}
CODE_SAMPLE: ${{ steps.parse.outputs.issueparser_code_sample }}
WHATSAPP_TYPE: ${{ steps.parse.outputs.issueparser_whatsapp_type }}
AUTH_TYPE: ${{ steps.parse.outputs.issueparser_auth_type }}
LIB_VERSION: ${{ steps.parse.outputs.issueparser_lib_version }}
WWEB_VERSION: ${{ steps.parse.outputs.issueparser_wweb_version }}
BROWSER_TYPE: ${{ steps.parse.outputs.issueparser_browser_type }}
BROWSER_VERSION: ${{ steps.parse.outputs.issueparser_browser_version }}
COMPUTER_OS: ${{ steps.parse.outputs.issueparser_computer_os }}
PHONE_OS: ${{ steps.parse.outputs.issueparser_phone_os }}
NODE_VERSION: ${{ steps.parse.outputs.issueparser_node_version }}
CHECKLIST: ${{ steps.parse.outputs.issueparser_checklist }}
run: |
{
echo 'body<<BODY_DELIMITER'
echo '## Issue Description'
echo "$DESCRIPTION"
echo ''
echo '## Reproduction Steps'
echo "$REPRODUCTION_STEPS"
echo ''
if [ -n "$CODE_SAMPLE" ]; then
echo '## Code Sample'
echo "$CODE_SAMPLE"
echo ''
fi
echo '## User Setup'
echo ''
echo '| WhatsApp | Type |'
echo '|---|---|'
[ -n "$WHATSAPP_TYPE" ] && echo "| **Account Type** | $WHATSAPP_TYPE |"
[ -n "$AUTH_TYPE" ] && echo "| **Authentication Strategy** | $AUTH_TYPE |"
[ -n "$WWEB_VERSION" ] && echo "| **WhatsApp Web Version** | $WWEB_VERSION |"
[ -n "$LIB_VERSION" ] && echo "| **whatsapp-web.js Version** | $LIB_VERSION |"
echo ''
echo '| Environment | Version |'
echo '|---|---|'
[ -n "$BROWSER_TYPE" ] && echo "| **Browser Type** | $BROWSER_TYPE |"
[ -n "$BROWSER_VERSION" ] && echo "| **Browser Version** | $BROWSER_VERSION |"
[ -n "$PHONE_OS" ] && echo "| **Phone OS Version** | $PHONE_OS |"
[ -n "$COMPUTER_OS" ] && echo "| **Running OS Version** | $COMPUTER_OS |"
[ -n "$NODE_VERSION" ] && echo "| **Node.js Version** | $NODE_VERSION |"
echo ''
if [ -n "$CHECKLIST" ]; then
echo '## Checklist'
echo '- [x] **I use the latest released version of whatsapp-web.js.**'
echo '- [x] I have searched existing issues and confirmed this is not a duplicate.'
echo '- [x] I have verified this is a library issue, not a problem with my code.'
fi
echo 'BODY_DELIMITER'
} >> "$GITHUB_OUTPUT"
- name: Update issue body
uses: julien-deramond/update-issue-body@v1
with:
issue-number: ${{ github.event.issue.number }}
body: ${{ steps.summary.outputs.body }}
edit-mode: replace
- name: Determine priority label
id: priority_label
env:
PRIORITY: ${{ steps.parse.outputs.issueparser_priority }}
run: |
if [[ "$PRIORITY" == "Low"* ]]; then
echo "label=issued low" >> "$GITHUB_OUTPUT"
elif [[ "$PRIORITY" == "Medium"* ]]; then
echo "label=issued medium" >> "$GITHUB_OUTPUT"
elif [[ "$PRIORITY" == "High"* ]]; then
echo "label=issued high" >> "$GITHUB_OUTPUT"
fi
- name: Add priority label
if: steps.priority_label.outputs.label != ''
uses: actions-ecosystem/action-add-labels@v1
with:
labels: ${{ steps.priority_label.outputs.label }}

24
.github/workflows/issue-triage.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Issue Triage
on:
issues:
types: [opened, edited, reopened]
permissions:
contents: read
issues: write
concurrency:
group: issue-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Label issue
uses: github/issue-labeler@v3.4
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/issue-labeler.yml
enable-versioned-regex: 0

29
.github/workflows/labels-sync.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: Sync Labels
on:
schedule:
- cron: '0 0 */3 * *'
workflow_dispatch:
push:
branches:
- main
paths:
- '.github/labels.yml'
permissions:
contents: read
issues: write
concurrency:
group: label-sync
cancel-in-progress: false
jobs:
label-sync:
runs-on: ubuntu-latest
if: github.repository_owner == 'pedroslopez'
steps:
- uses: actions/checkout@v6
- uses: crazy-max/ghaction-github-labeler@v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,20 +1,20 @@
name: Lint
on:
push:
pull_request:
push:
pull_request:
jobs:
eslint:
name: ESLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install node v14
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
run: npm install
- name: Run ESLint
run: ./node_modules/.bin/eslint .
eslint:
name: ESLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install node v14
uses: actions/setup-node@v2
with:
node-version: '14'
- name: Install dependencies
run: npm install
- name: Run ESLint
run: ./node_modules/.bin/eslint .

49
.github/workflows/pr-triage.yml vendored Normal file
View file

@ -0,0 +1,49 @@
name: PR Triage
on:
pull_request_target:
types:
- opened
- edited
- reopened
- synchronize
permissions:
contents: read
pull-requests: write
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
label:
name: Label PR
if: github.event.action != 'edited'
runs-on: ubuntu-latest
steps:
- name: Label pull request
uses: actions/labeler@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/pr-labeler.yml
dot: true
sync-labels: true
validate-title:
name: Validate title
runs-on: ubuntu-latest
steps:
- name: Validate pull request title
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
REGEX="^(revert: )?(feat|fix|docs|style|refactor|perf|test|build|ci|chore|types|infra)(\\(.+\\))?!?: .{1,72}$"
echo "Title: \"$TITLE\""
shopt -s nocasematch
if [[ ! "$TITLE" =~ $REGEX ]]; then
echo "❌ PR title does not match the required format!"
exit 1
fi

View file

@ -1,37 +1,55 @@
name: release
name: Release a New Version
on:
workflow_dispatch:
inputs:
release_type:
description: "`alpha`, `alpha-minor`, `alpha-major`, `release-minor`, `release-patch` release?"
required: true
default: "alpha"
workflow_dispatch:
inputs:
release_type:
description: 'Release type'
required: true
default: 'alpha'
type: choice
options:
- alpha
- alpha-minor
- alpha-major
- release-minor
- release-patch
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
if: ${{ github.repository == 'pedroslopez/whatsapp-web.js' }}
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- run: git config --global user.email "hello@wwebjs.dev"
- run: git config --global user.name "WWebJS Bot"
- name: Bump version and publish to NPM
id: publish
run: ./tools/publish ${{ github.event.inputs.release_type }}
- name: Create GitHub Release
id: create_release
uses: ncipollo/release-action@v1
with:
prerelease: ${{ steps.publish.outputs.PRERELEASE }}
generateReleaseNotes: true
tag: v${{ steps.publish.outputs.NEW_VERSION }}
release:
runs-on: ubuntu-latest
if: github.repository == 'pedroslopez/whatsapp-web.js'
permissions:
contents: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.RELEASE_GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Configure Git
run: |
git config --global user.email "hello@wwebjs.dev"
git config --global user.name "WWebJS Bot"
- name: Bump version and publish to NPM
id: publish
run: ./tools/publish ${{ inputs.release_type }}
- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
prerelease: ${{ steps.publish.outputs.PRERELEASE }}
generateReleaseNotes: true
tag: v${{ steps.publish.outputs.NEW_VERSION }}

42
.github/workflows/stale.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Mark Stale Issues and PRs
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: stale
cancel-in-progress: false
jobs:
stale:
if: github.repository_owner == 'pedroslopez'
runs-on: ubuntu-latest
steps:
- name: Mark stale issues and PRs
uses: actions/stale@v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 35
days-before-close: 7
stale-issue-label: 'stale'
stale-pr-label: 'stale'
start-date: '2026-03-03T00:00:00Z'
remove-stale-when-updated: true
exempt-issue-labels: 'pinned,security,feature request,issued high'
exempt-pr-labels: 'pinned,security,whatsapp change,approved,waiting for testers'
stale-issue-message: >
Marking this issue as stale due to inactivity. Comment or update the issue to
keep it open. It will be closed in 7 days otherwise. Thank you for your contributions.
stale-pr-message: >
Marking this pull request as stale due to inactivity. Push changes or comment
to keep it open. It will be closed in 7 days otherwise. Thank you for your contributions.
close-issue-message: >
Closing this issue due to inactivity. Feel free to reopen if it is still relevant.
close-pr-message: >
Closing this pull request due to inactivity. You can reopen it anytime to continue.

28
.github/workflows/tests.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: Tests
on:
push:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Node.js v20
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm install
- name: Run Eslint
run: npm run lint

View file

@ -1,39 +1,54 @@
name: Update
on:
schedule:
- cron: "0 12 * * 0"
workflow_dispatch:
schedule:
- cron: '0 12 * * 0'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
concurrency:
group: wa-update
cancel-in-progress: false
jobs:
update:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./tools/version-checker
update:
if: github.repository_owner == 'pedroslopez'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./tools/version-checker
steps:
- uses: actions/checkout@v2
- name: Install node v16
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Run Updater
run: ./update-version
- name: Store WA Version
run: echo WA_VERSION=`cat ./.version` >> $GITHUB_ENV
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
branch: auto-wa-web-update/patch
delete-branch: true
commit-message: Update supported WhatsApp Web version to v${{ env.WA_VERSION }}
title: Update WhatsApp Web Version (${{ env.WA_VERSION }})
body: |
A new version of WhatsApp Web has been detected!
steps:
- name: Checkout
uses: actions/checkout@v4
Tests should be run against this new version before merging.
labels: WhatsApp Change
reviewers: pedroslopez
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm install
- name: Run Updater
run: ./update-version
- name: Store WA Version
run: echo "WA_VERSION=$(cat ./.version)" >> $GITHUB_ENV
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
branch: auto-wa-web-update/patch
delete-branch: true
commit-message: 'chore: update supported WhatsApp Web version to v${{ env.WA_VERSION }}'
title: 'chore: update WhatsApp Web version (${{ env.WA_VERSION }})'
body: |
A new version of WhatsApp Web has been detected!
Tests should be run against this new version before merging.
labels: whatsapp change
reviewers: pedroslopez

4
.husky/commit-msg Normal file
View file

@ -0,0 +1,4 @@
# Normalize commit type to lowercase
sed -E 's/^[A-Za-z]+/\L&/' "$1" > "$1.tmp" && mv "$1.tmp" "$1"
npx --no-install commitlint --edit "$1"

1
.husky/pre-commit Normal file
View file

@ -0,0 +1 @@
npx lint-staged

View file

@ -8,9 +8,7 @@
"includePattern": ".js$",
"excludePattern": "(node_modules/|docs)"
},
"plugins": [
"plugins/markdown"
],
"plugins": ["plugins/markdown"],
"templates": {
"cleverLinks": false,
"monospaceLinks": true,

6
.lintstagedrc.json Normal file
View file

@ -0,0 +1,6 @@
{
"*": "prettier --ignore-unknown --write",
"*.{js,ts}": "eslint --fix",
"src/**/*.{js,ts}": "eslint --fix",
"tests/**/*.{js,ts}": "eslint --fix"
}

8
.prettierignore Normal file
View file

@ -0,0 +1,8 @@
node_modules
dist
coverage
*.min.js
.wa-version
.wwebjs_auth
.wwebjs_cache
docs

10
.prettierrc.json Normal file
View file

@ -0,0 +1,10 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 4,
"useTabs": false,
"arrowParens": "always",
"endOfLine": "lf"
}

157
README.md
View file

@ -1,59 +1,65 @@
<div align="center">
<br />
<p>
<a href="https://wwebjs.dev"><img src="https://github.com/wwebjs/assets/blob/main/Collection/GitHub/wwebjs.png?raw=true" title="whatsapp-web.js" alt="WWebJS Website" width="500" /></a>
<a href="https://wwebjs.dev">
<img src="https://github.com/wwebjs/Assets/blob/main/Collection/GitHub/whatsapp-web.js.png?raw=true"
title="whatsapp-web.js" alt="WWebJS Website" />
</a>
</p>
<br />
<p>
<a href="https://www.npmjs.com/package/whatsapp-web.js"><img src="https://img.shields.io/npm/v/whatsapp-web.js.svg" alt="npm" /></a>
<a href="https://depfu.com/github/pedroslopez/whatsapp-web.js?project_id=9765"><img src="https://badges.depfu.com/badges/4a65a0de96ece65fdf39e294e0c8dcba/overview.svg" alt="Depfu" /></a>
<img src="https://img.shields.io/badge/WhatsApp_Web-2.3000.1017054665-brightgreen.svg" alt="WhatsApp_Web 2.2346.52" />
<a href="https://discord.gg/H7DqQs4"><img src="https://img.shields.io/discord/698610475432411196.svg?logo=discord" alt="Discord server" /></a>
</p>
<br />
<a href="https://www.npmjs.com/package/whatsapp-web.js"><img
src="https://img.shields.io/npm/v/whatsapp-web.js.svg" alt="npm" /></a>
<a href="https://www.npmjs.com/package/whatsapp-web.js"><img alt="NPM Downloads"
src="https://img.shields.io/npm/d18m/whatsapp-web.js" /></a>
<a href="https://github.com/pedroslopez/whatsapp-web.js/graphs/contributors"><img alt="GitHub contributors"
src="https://img.shields.io/github/contributors-anon/pedroslopez/whatsapp-web.js" /></a>
<a href="https://depfu.com/github/pedroslopez/whatsapp-web.js?project_id=9765"><img
src="https://badges.depfu.com/badges/4a65a0de96ece65fdf39e294e0c8dcba/overview.svg" alt="Depfu" /></a>
<a href="https://discord.wwebjs.dev"><img
src="https://img.shields.io/discord/698610475432411196.svg?logo=discord" alt="Discord server" /></a>
</p>
</div>
## About
**A WhatsApp API client that operates via the WhatsApp Web browser.**
The library launches the WhatsApp Web browser app via Puppeteer, accessing its internal functions and creating a managed instance to reduce the risk of being blocked. This gives the API client nearly all WhatsApp Web features for dynamic use in a Node.js application.
> [!IMPORTANT]
> **It is not guaranteed you will not be blocked by using this method. WhatsApp does not allow bots or unofficial clients on their platform, so this shouldn't be considered totally safe.**
whatsapp‑web.js is a powerful [Node.js][nodejs] library that lets you interact with WhatsApp Web, making it easy to build a dynamic WhatsApp API with nearly all features of the web client. It uses [Puppeteer][puppeteer] to access WhatsApp Web’s internal functions and runs them in a managed browser instance to reduce the risk of being blocked.
## Links
* [GitHub][gitHub]
* [Guide][guide] ([source][guide-source])
* [Documentation][documentation] ([source][documentation-source])
* [Discord Server][discord]
* [npm][npm]
- [GitHub][gitHub]
- [Guide][guide] ([source][guide-source])
- [Documentation][documentation] ([source][documentation-source])
- [Discord Server][discord]
- [npm][npm]
## Installation
The module is available on [npm][npm] via `npm i whatsapp-web.js`!
**Node.js `v18.0.0` or higher, is required.**
> [!NOTE]
> **Node ``v18`` or higher, is required.**
> See the [Guide][guide] for quick upgrade instructions.
```sh
npm install whatsapp-web.js
yarn add whatsapp-web.js
pnpm add whatsapp-web.js
```
Having trouble installing? Take a peak at the [Guide][guide] for more detailed instructions.
## Example usage
```js
const { Client } = require('whatsapp-web.js');
const qrcode = require('qrcode-terminal');
const client = new Client();
client.on('qr', (qr) => {
// Generate and scan this code with your phone
console.log('QR RECEIVED', qr);
qrcode.generate(qr, { small: true });
});
client.on('ready', () => {
console.log('Client is ready!');
});
client.on('message', msg => {
client.on('message', (msg) => {
if (msg.body == '!ping') {
msg.reply('pong');
}
@ -62,61 +68,59 @@ client.on('message', msg => {
client.initialize();
```
Take a look at [example.js][examples] for another examples with additional use cases.
For further details on saving and restoring sessions, explore the provided [Authentication Strategies][auth-strategies].
Take a look at [example.js][examples] for additional examples and use cases.
For more details on saving and restoring sessions, check out the [Authentication Strategies][auth-strategies].
## Supported features
| Feature | Status |
| ------------- | ------------- |
| Multi Device | ✅ |
| Send messages | ✅ |
| Receive messages | ✅ |
| Send media (images/audio/documents) | ✅ |
| Send media (video) | ✅ [(requires Google Chrome)][google-chrome] |
| Send stickers | ✅ |
| Receive media (images/audio/video/documents) | ✅ |
| Send contact cards | ✅ |
| Send location | ✅ |
| Send buttons | ❌ [(DEPRECATED)][deprecated-video] |
| Send lists | ❌ [(DEPRECATED)][deprecated-video] |
| Receive location | ✅ |
| Message replies | ✅ |
| Join groups by invite | ✅ |
| Get invite for group | ✅ |
| Modify group info (subject, description) | ✅ |
| Modify group settings (send messages, edit info) | ✅ |
| Add group participants | ✅ |
| Kick group participants | ✅ |
| Promote/demote group participants | ✅ |
| Mention users | ✅ |
| Mention groups | ✅ |
| Mute/unmute chats | ✅ |
| Block/unblock contacts | ✅ |
| Get contact info | ✅ |
| Get profile pictures | ✅ |
| Set user status message | ✅ |
| React to messages | ✅ |
| Create polls | ✅ |
| Channels | ✅ |
| Vote in polls | 🔜 |
| Communities | 🔜 |
| Feature | Status |
| ------------------------------------------------ | -------------------------------------------- |
| Multi Device | ✅ |
| Send messages | ✅ |
| Receive messages | ✅ |
| Send media (images/audio/documents) | ✅ |
| Send media (video) | ✅ [(requires Google Chrome)][google-chrome] |
| Send stickers | ✅ |
| Receive media (images/audio/video/documents) | ✅ |
| Send contact cards | ✅ |
| Send location | ✅ |
| Send buttons | ❌ [(DEPRECATED)][deprecated-video] |
| Send lists | ❌ [(DEPRECATED)][deprecated-video] |
| Receive location | ✅ |
| Message replies | ✅ |
| Join groups by invite | ✅ |
| Get invite for group | ✅ |
| Modify group info (subject, description) | ✅ |
| Modify group settings (send messages, edit info) | ✅ |
| Add group participants | ✅ |
| Kick group participants | ✅ |
| Promote/demote group participants | ✅ |
| Mention users | ✅ |
| Mention groups | ✅ |
| Mute/unmute chats | ✅ |
| Block/unblock contacts | ✅ |
| Get contact info | ✅ |
| Get profile pictures | ✅ |
| Set user status message | ✅ |
| React to messages | ✅ |
| Create polls | ✅ |
| Channels | ✅ |
| Vote in polls | ✅ |
| Communities | 🔜 |
Something missing? Make an issue and let us know!
## Supporting the project
You can support the maintainer of this project through the links below:
- [Support via GitHub Sponsors][gitHub-sponsors]
- [Support via PayPal][support-payPal]
## Contributing
Feel free to open pull requests; we welcome contributions! However, for significant changes, it's best to open an issue beforehand. Make sure to review our [contribution guidelines][contributing] before creating a pull request. Before creating your own issue or pull request, always check to see if one already exists!
## Supporting the project
You can support the maintainer of this project through the links below
- [Support via GitHub Sponsors][gitHub-sponsors]
- [Support via PayPal][support-payPal]
- [Sign up for DigitalOcean][digitalocean] and get $200 in credit when you sign up (Referral)
## Disclaimer
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. The official WhatsApp website can be found at [whatsapp.com][whatsapp]. "WhatsApp" as well as related names, marks, emblems and images are registered trademarks of their respective owners. Also it is not guaranteed you will not be blocked by using this method. WhatsApp does not allow bots or unofficial clients on their platform, so this shouldn't be considered totally safe.
@ -127,7 +131,7 @@ Copyright 2019 Pedro S Lopez
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this project except in compliance with the License.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
You may obtain a copy of the License at <https://www.apache.org/licenses/LICENSE-2.0>.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
@ -135,21 +139,20 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
[guide]: https://guide.wwebjs.dev/guide
[guide-source]: https://github.com/wwebjs/wwebjs.dev/tree/main
[documentation]: https://docs.wwebjs.dev/
[documentation-source]: https://github.com/pedroslopez/whatsapp-web.js/tree/main/docs
[discord]: https://discord.gg/H7DqQs4
[discord]: https://discord.wwebjs.dev
[gitHub]: https://github.com/pedroslopez/whatsapp-web.js
[npm]: https://npmjs.org/package/whatsapp-web.js
[nodejs]: https://nodejs.org/en/download/
[examples]: https://github.com/pedroslopez/whatsapp-web.js/blob/master/example.js
[examples]: https://github.com/pedroslopez/whatsapp-web.js/blob/main/example.js
[auth-strategies]: https://wwebjs.dev/guide/creating-your-bot/authentication.html
[google-chrome]: https://wwebjs.dev/guide/creating-your-bot/handling-attachments.html#caveat-for-sending-videos-and-gifs
[deprecated-video]: https://www.youtube.com/watch?v=hv1R1rLeVVE
[gitHub-sponsors]: https://github.com/sponsors/pedroslopez
[support-payPal]: https://www.paypal.me/psla/
[digitalocean]: https://m.do.co/c/73f906a36ed4
[contributing]: https://github.com/pedroslopez/whatsapp-web.js/blob/main/CODE_OF_CONDUCT.md
[contributing]: .github/CONTRIBUTING.md
[whatsapp]: https://whatsapp.com
[puppeteer]: https://pptr.dev/

29
commitlint.config.js Normal file
View file

@ -0,0 +1,29 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'header-max-length': [2, 'always', 72],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'perf',
'test',
'build',
'ci',
'chore',
'types',
'revert',
'infra',
],
],
'type-case': [0],
'scope-case': [0],
'subject-case': [0],
'subject-exclamation-mark': [0],
},
};

View file

@ -38,14 +38,14 @@ client.on('qr', async (qr) => {
});
client.on('code', (code) => {
console.log('Pairing code:',code);
console.log('Pairing code:', code);
});
client.on('authenticated', () => {
console.log('AUTHENTICATED');
});
client.on('auth_failure', msg => {
client.on('auth_failure', (msg) => {
// Fired if session restore was unsuccessful
console.error('AUTHENTICATION FAILURE', msg);
});
@ -55,26 +55,23 @@ client.on('ready', async () => {
const debugWWebVersion = await client.getWWebVersion();
console.log(`WWebVersion = ${debugWWebVersion}`);
client.pupPage.on('pageerror', function(err) {
client.pupPage.on('pageerror', function (err) {
console.log('Page error: ' + err.toString());
});
client.pupPage.on('error', function(err) {
client.pupPage.on('error', function (err) {
console.log('Page error: ' + err.toString());
});
});
client.on('message', async msg => {
client.on('message', async (msg) => {
console.log('MESSAGE RECEIVED', msg);
if (msg.body === '!ping reply') {
// Send a new message as a reply to the current one
msg.reply('pong');
} else if (msg.body === '!ping') {
// Send a new message to the same chat
client.sendMessage(msg.from, 'pong');
} else if (msg.body.startsWith('!sendto ')) {
// Direct send a new message to specific id
let number = msg.body.split(' ')[1];
@ -84,7 +81,6 @@ client.on('message', async msg => {
let chat = await msg.getChat();
chat.sendSeen();
client.sendMessage(number, message);
} else if (msg.body.startsWith('!subject ')) {
// Change the group subject
let chat = await msg.getChat();
@ -127,7 +123,11 @@ client.on('message', async msg => {
}
} else if (msg.body.startsWith('!addmembers')) {
const group = await msg.getChat();
const result = await group.addParticipants(['number1@c.us', 'number2@c.us', 'number3@c.us']);
const result = await group.addParticipants([
'number1@c.us',
'number2@c.us',
'number3@c.us',
]);
/**
* The example of the {@link result} output:
*
@ -154,8 +154,15 @@ client.on('message', async msg => {
*/
console.log(result);
} else if (msg.body === '!creategroup') {
const partitipantsToAdd = ['number1@c.us', 'number2@c.us', 'number3@c.us'];
const result = await client.createGroup('Group Title', partitipantsToAdd);
const partitipantsToAdd = [
'number1@c.us',
'number2@c.us',
'number3@c.us',
];
const result = await client.createGroup(
'Group Title',
partitipantsToAdd,
);
/**
* The example of the {@link result} output:
* {
@ -216,12 +223,15 @@ client.on('message', async msg => {
client.sendMessage(msg.from, `The bot has ${chats.length} chats open.`);
} else if (msg.body === '!info') {
let info = client.info;
client.sendMessage(msg.from, `
client.sendMessage(
msg.from,
`
*Connection info*
User name: ${info.pushname}
My number: ${info.wid.user}
Platform: ${info.platform}
`);
`,
);
} else if (msg.body === '!mediainfo' && msg.hasMedia) {
const attachmentData = await msg.downloadMedia();
msg.reply(`
@ -244,11 +254,15 @@ client.on('message', async msg => {
const quotedMsg = await msg.getQuotedMessage();
if (quotedMsg.hasMedia) {
const attachmentData = await quotedMsg.downloadMedia();
client.sendMessage(msg.from, attachmentData, { caption: 'Here\'s your requested media.' });
client.sendMessage(msg.from, attachmentData, {
caption: "Here's your requested media.",
});
}
if (quotedMsg.hasMedia && quotedMsg.type === 'audio') {
const audio = await quotedMsg.downloadMedia();
await client.sendMessage(msg.from, audio, { sendAudioAsVoice: true });
await client.sendMessage(msg.from, audio, {
sendAudioAsVoice: true,
});
}
} else if (msg.body === '!isviewonce' && msg.hasQuotedMsg) {
const quotedMsg = await msg.getQuotedMessage();
@ -262,9 +276,19 @@ client.on('message', async msg => {
// location with name only
await msg.reply(new Location(37.422, -122.084, { name: 'Googleplex' }));
// location with address only
await msg.reply(new Location(37.422, -122.084, { address: '1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA' }));
await msg.reply(
new Location(37.422, -122.084, {
address: '1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA',
}),
);
// location with name, address and url
await msg.reply(new Location(37.422, -122.084, { name: 'Googleplex', address: '1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA', url: 'https://google.com' }));
await msg.reply(
new Location(37.422, -122.084, {
name: 'Googleplex',
address: '1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA',
url: 'https://google.com',
}),
);
} else if (msg.location) {
msg.reply(msg.location);
} else if (msg.body.startsWith('!status ')) {
@ -279,11 +303,11 @@ client.on('message', async msg => {
* without wrapping it in Array, and a user's phone number to the message body:
*/
await chat.sendMessage(`Hi @${userNumber}`, {
mentions: userNumber + '@c.us'
mentions: userNumber + '@c.us',
});
// To mention a list of users:
await chat.sendMessage(`Hi @${userNumber}, @${userNumber}`, {
mentions: [userNumber + '@c.us', userNumber + '@c.us']
mentions: [userNumber + '@c.us', userNumber + '@c.us'],
});
} else if (msg.body === '!mentionGroups') {
const chat = await msg.getChat();
@ -299,21 +323,28 @@ client.on('message', async msg => {
* To mention one group:
*/
await chat.sendMessage(`Check the last message here: @${groupId}`, {
groupMentions: { subject: 'GroupSubject', id: groupId }
groupMentions: { subject: 'GroupSubject', id: groupId },
});
// To mention a list of groups:
await chat.sendMessage(`Check the last message in these groups: @${groupId}, @${groupId}`, {
groupMentions: [
{ subject: 'FirstGroup', id: groupId },
{ subject: 'SecondGroup', id: groupId }
]
});
await chat.sendMessage(
`Check the last message in these groups: @${groupId}, @${groupId}`,
{
groupMentions: [
{ subject: 'FirstGroup', id: groupId },
{ subject: 'SecondGroup', id: groupId },
],
},
);
} else if (msg.body === '!getGroupMentions') {
// To get group mentions from a message:
const groupId = 'ZZZZZZZZZZ@g.us';
const msg = await client.sendMessage('chatId', `Check the last message here: @${groupId}`, {
groupMentions: { subject: 'GroupSubject', id: groupId }
});
const msg = await client.sendMessage(
'chatId',
`Check the last message here: @${groupId}`,
{
groupMentions: { subject: 'GroupSubject', id: groupId },
},
);
/** {@link groupMentions} is an array of `GroupChat` */
const groupMentions = await msg.getGroupMentions();
console.log(groupMentions);
@ -356,13 +387,30 @@ client.on('message', async msg => {
client.interface.openChatWindowAt(quotedMsg.id._serialized);
}
} else if (msg.body === '!buttons') {
let button = new Buttons('Button body', [{ body: 'bt1' }, { body: 'bt2' }, { body: 'bt3' }], 'title', 'footer');
let button = new Buttons(
'Button body',
[{ body: 'bt1' }, { body: 'bt2' }, { body: 'bt3' }],
'title',
'footer',
);
client.sendMessage(msg.from, button);
} else if (msg.body === '!list') {
let sections = [
{ title: 'sectionTitle', rows: [{ title: 'ListItem1', description: 'desc' }, { title: 'ListItem2' }] }
{
title: 'sectionTitle',
rows: [
{ title: 'ListItem1', description: 'desc' },
{ title: 'ListItem2' },
],
},
];
let list = new List('List body', 'btnText', sections, 'Title', 'footer');
let list = new List(
'List body',
'btnText',
sections,
'Title',
'footer',
);
client.sendMessage(msg.from, list);
} else if (msg.body === '!reaction') {
await msg.react('👍');
@ -370,7 +418,11 @@ client.on('message', async msg => {
/** By default the poll is created as a single choice poll: */
await msg.reply(new Poll('Winter or Summer?', ['Winter', 'Summer']));
/** If you want to provide a multiple choice poll, add allowMultipleAnswers as true: */
await msg.reply(new Poll('Cats or Dogs?', ['Cats', 'Dogs'], { allowMultipleAnswers: true }));
await msg.reply(
new Poll('Cats or Dogs?', ['Cats', 'Dogs'], {
allowMultipleAnswers: true,
}),
);
/**
* You can provide a custom message secret, it can be used as a poll ID:
* @note It has to be a unique vector with a length of 32
@ -378,9 +430,10 @@ client.on('message', async msg => {
await msg.reply(
new Poll('Cats or Dogs?', ['Cats', 'Dogs'], {
messageSecret: [
1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]
})
1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
],
}),
);
} else if (msg.body === '!vote') {
if (msg.hasQuotedMsg) {
@ -417,13 +470,17 @@ client.on('message', async msg => {
* Presented an example for membership request approvals, the same examples are for the request rejections.
* To approve the membership request from a specific user:
*/
await client.approveGroupMembershipRequests(msg.from, { requesterIds: 'number@c.us' });
await client.approveGroupMembershipRequests(msg.from, {
requesterIds: 'number@c.us',
});
/** The same for execution on group object (no need to provide the group ID): */
const group = await msg.getChat();
await group.approveGroupMembershipRequests({ requesterIds: 'number@c.us' });
await group.approveGroupMembershipRequests({
requesterIds: 'number@c.us',
});
/** To approve several membership requests: */
const approval = await client.approveGroupMembershipRequests(msg.from, {
requesterIds: ['number1@c.us', 'number2@c.us']
requesterIds: ['number1@c.us', 'number2@c.us'],
});
/**
* The example of the {@link approval} output:
@ -446,17 +503,17 @@ client.on('message', async msg => {
/** To change the sleep value to 300 ms: */
await client.approveGroupMembershipRequests(msg.from, {
requesterIds: ['number1@c.us', 'number2@c.us'],
sleep: 300
sleep: 300,
});
/** To change the sleep value to random value between 100 and 300 ms: */
await client.approveGroupMembershipRequests(msg.from, {
requesterIds: ['number1@c.us', 'number2@c.us'],
sleep: [100, 300]
sleep: [100, 300],
});
/** To explicitly disable the sleep: */
await client.approveGroupMembershipRequests(msg.from, {
requesterIds: ['number1@c.us', 'number2@c.us'],
sleep: null
sleep: null,
});
} else if (msg.body === '!pinmsg') {
/**
@ -483,7 +540,11 @@ client.on('message', async msg => {
// const chat = await client.getChatById(msg.from);
// const isSynced = await chat.syncHistory();
await msg.reply(isSynced ? 'Historical chat is syncing..' : 'There is no historical chat to sync.');
await msg.reply(
isSynced
? 'Historical chat is syncing..'
: 'There is no historical chat to sync.',
);
} else if (msg.body === '!statuses') {
const statuses = await client.getBroadcasts();
console.log(statuses);
@ -526,10 +587,14 @@ client.on('message', async msg => {
} else if (msg.body === '!postStatus') {
await client.sendMessage('status@broadcast', 'Hello there!');
// send with a different style
await client.sendMessage('status@broadcast', 'Hello again! Looks different?', {
fontStyle: 1,
backgroundColor: '#0b3296'
});
await client.sendMessage(
'status@broadcast',
'Hello again! Looks different?',
{
fontStyle: 1,
backgroundColor: '#0b3296',
},
);
}
});
@ -604,7 +669,7 @@ client.on('group_update', (notification) => {
console.log('update', notification);
});
client.on('change_state', state => {
client.on('change_state', (state) => {
console.log('CHANGE STATE', state);
});
@ -614,7 +679,10 @@ let rejectCalls = true;
client.on('call', async (call) => {
console.log('Call received, rejecting. GOTO Line 261 to disable', call);
if (rejectCalls) await call.reject();
await client.sendMessage(call.from, `[${call.fromMe ? 'Outgoing' : 'Incoming'}] Phone call from ${call.from}, type ${call.isGroup ? 'group' : ''} ${call.isVideo ? 'video' : 'audio'} call. ${rejectCalls ? 'This call was automatically rejected by the script.' : ''}`);
await client.sendMessage(
call.from,
`[${call.fromMe ? 'Outgoing' : 'Incoming'}] Phone call from ${call.from}, type ${call.isGroup ? 'group' : ''} ${call.isVideo ? 'video' : 'audio'} call. ${rejectCalls ? 'This call was automatically rejected by the script.' : ''}`,
);
});
client.on('disconnected', (reason) => {
@ -623,14 +691,19 @@ client.on('disconnected', (reason) => {
client.on('contact_changed', async (message, oldId, newId, isContact) => {
/** The time the event occurred. */
const eventTime = (new Date(message.timestamp * 1000)).toLocaleString();
const eventTime = new Date(message.timestamp * 1000).toLocaleString();
console.log(
`The contact ${oldId.slice(0, -5)}` +
`${!isContact ? ' that participates in group ' +
`${(await client.getChatById(message.to ?? message.from)).name} ` : ' '}` +
`changed their phone number\nat ${eventTime}.\n` +
`Their new phone number is ${newId.slice(0, -5)}.\n`);
`${
!isContact
? ' that participates in group ' +
`${(await client.getChatById(message.to ?? message.from)).name} `
: ' '
}` +
`changed their phone number\nat ${eventTime}.\n` +
`Their new phone number is ${newId.slice(0, -5)}.\n`,
);
/**
* Information about the @param {message}:
@ -661,9 +734,9 @@ client.on('contact_changed', async (message, oldId, newId, isContact) => {
client.on('group_admin_changed', (notification) => {
if (notification.type === 'promote') {
/**
* Emitted when a current user is promoted to an admin.
* {@link notification.author} is a user who performs the action of promoting/demoting the current user.
*/
* Emitted when a current user is promoted to an admin.
* {@link notification.author} is a user who performs the action of promoting/demoting the current user.
*/
console.log(`You were promoted by ${notification.author}`);
} else if (notification.type === 'demote')
/** Emitted when a current user is demoted to a regular user. */
@ -692,8 +765,14 @@ client.on('group_membership_request', async (notification) => {
*/
console.log(notification);
/** You can approve or reject the newly appeared membership request: */
await client.approveGroupMembershipRequestss(notification.chatId, notification.author);
await client.rejectGroupMembershipRequests(notification.chatId, notification.author);
await client.approveGroupMembershipRequestss(
notification.chatId,
notification.author,
);
await client.rejectGroupMembershipRequests(
notification.chatId,
notification.author,
);
});
client.on('message_reaction', async (reaction) => {

1559
index.d.ts vendored

File diff suppressed because it is too large Load diff

View file

@ -31,5 +31,5 @@ module.exports = {
LocalAuth: require('./src/authStrategies/LocalAuth'),
RemoteAuth: require('./src/authStrategies/RemoteAuth'),
...Constants
...Constants,
};

View file

@ -1,58 +1,72 @@
{
"name": "whatsapp-web.js",
"version": "1.34.6",
"description": "Library for interacting with the WhatsApp Web API ",
"main": "./index.js",
"typings": "./index.d.ts",
"scripts": {
"test": "mocha tests --recursive --timeout 5000",
"test-single": "mocha",
"shell": "node --experimental-repl-await ./shell.js",
"generate-docs": "npx jsdoc --configure .jsdoc.json --verbose"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pedroslopez/whatsapp-web.js.git"
},
"keywords": [
"whatsapp",
"whatsapp-web",
"api",
"bot",
"client",
"node"
],
"author": "Pedro Lopez",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/pedroslopez/whatsapp-web.js/issues"
},
"homepage": "https://wwebjs.dev/",
"dependencies": {
"fluent-ffmpeg": "2.1.3",
"mime": "^3.0.0",
"node-fetch": "^2.6.9",
"node-webpmux": "3.1.7",
"puppeteer": "^24.31.0"
},
"devDependencies": {
"@types/node-fetch": "^2.5.12",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"dotenv": "^16.0.0",
"eslint": "^8.4.1",
"eslint-plugin-mocha": "^10.0.3",
"jsdoc": "^3.6.4",
"jsdoc-baseline": "^0.1.5",
"mocha": "^9.0.2",
"sinon": "^13.0.1"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"archiver": "^5.3.1",
"fs-extra": "^10.1.0",
"unzipper": "^0.10.11"
}
"name": "whatsapp-web.js",
"version": "1.34.6",
"description": "A Node.js library for interacting with WhatsApp Web",
"main": "./index.js",
"typings": "./index.d.ts",
"scripts": {
"test": "mocha tests --recursive --timeout 5000",
"test-single": "mocha",
"shell": "node --experimental-repl-await ./shell.js",
"generate-docs": "npx jsdoc --configure .jsdoc.json --verbose",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"check": "npm run lint && npm run format:check",
"prepare": "is-ci || husky"
},
"repository": {
"type": "git",
"url": "git+https://github.com/pedroslopez/whatsapp-web.js.git"
},
"keywords": [
"whatsapp",
"whatsapp-web",
"api",
"bot",
"client",
"node"
],
"author": "Pedro Lopez",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/pedroslopez/whatsapp-web.js/issues"
},
"homepage": "https://wwebjs.dev/",
"dependencies": {
"fluent-ffmpeg": "2.1.3",
"mime": "^3.0.0",
"node-fetch": "^2.6.9",
"node-webpmux": "3.1.7",
"puppeteer": "^24.31.0"
},
"devDependencies": {
"is-ci": "^4.1.0",
"@commitlint/cli": "^20.4.1",
"@commitlint/config-conventional": "^20.4.1",
"@types/node-fetch": "^2.5.12",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"dotenv": "^16.0.0",
"eslint": "^8.4.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-mocha": "^10.0.3",
"eslint-plugin-prettier": "^5.5.5",
"husky": "^9.1.7",
"jsdoc": "^3.6.4",
"jsdoc-baseline": "^0.1.5",
"lint-staged": "^16.2.7",
"mocha": "^9.0.2",
"prettier": "^3.8.1",
"sinon": "^13.0.1"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"archiver": "^5.3.1",
"fs-extra": "^10.1.0",
"unzipper": "^0.10.11"
}
}

View file

@ -12,7 +12,7 @@ const { Client, LocalAuth } = require('./index');
const client = new Client({
puppeteer: { headless: false },
authStrategy: new LocalAuth()
authStrategy: new LocalAuth(),
});
console.log('Initializing...');

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@ class BaseAuthStrategy {
return {
failed: false,
restart: false,
failureEventPayload: undefined
failureEventPayload: undefined,
};
}
async getAuthEventPayload() {}

View file

@ -10,14 +10,16 @@ const BaseAuthStrategy = require('./BaseAuthStrategy');
* @param {string} options.clientId - Client id to distinguish instances if you are using multiple, otherwise keep null if you are using only one instance
* @param {string} options.dataPath - Change the default path for saving session files, default is: "./.wwebjs_auth/"
* @param {number} options.rmMaxRetries - Sets the maximum number of retries for removing the session directory
*/
*/
class LocalAuth extends BaseAuthStrategy {
constructor({ clientId, dataPath, rmMaxRetries }={}) {
constructor({ clientId, dataPath, rmMaxRetries } = {}) {
super();
const idRegex = /^[-_\w]+$/i;
if(clientId && !idRegex.test(clientId)) {
throw new Error('Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.');
if (clientId && !idRegex.test(clientId)) {
throw new Error(
'Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.',
);
}
this.dataPath = path.resolve(dataPath || './.wwebjs_auth/');
@ -27,18 +29,25 @@ class LocalAuth extends BaseAuthStrategy {
async beforeBrowserInitialized() {
const puppeteerOpts = this.client.options.puppeteer;
const sessionDirName = this.clientId ? `session-${this.clientId}` : 'session';
const sessionDirName = this.clientId
? `session-${this.clientId}`
: 'session';
const dirPath = path.join(this.dataPath, sessionDirName);
if(puppeteerOpts.userDataDir && puppeteerOpts.userDataDir !== dirPath) {
throw new Error('LocalAuth is not compatible with a user-supplied userDataDir.');
if (
puppeteerOpts.userDataDir &&
puppeteerOpts.userDataDir !== dirPath
) {
throw new Error(
'LocalAuth is not compatible with a user-supplied userDataDir.',
);
}
fs.mkdirSync(dirPath, { recursive: true });
this.client.options.puppeteer = {
...puppeteerOpts,
userDataDir: dirPath
userDataDir: dirPath,
};
this.userDataDir = dirPath;
@ -46,13 +55,17 @@ class LocalAuth extends BaseAuthStrategy {
async logout() {
if (this.userDataDir) {
await fs.promises.rm(this.userDataDir, { recursive: true, force: true, maxRetries: this.rmMaxRetries })
await fs.promises
.rm(this.userDataDir, {
recursive: true,
force: true,
maxRetries: this.rmMaxRetries,
})
.catch((e) => {
throw new Error(e);
});
}
}
}
module.exports = LocalAuth;

View file

@ -5,8 +5,7 @@ const BaseAuthStrategy = require('./BaseAuthStrategy');
/**
* No session restoring functionality
* Will need to authenticate via QR code every time
*/
class NoAuth extends BaseAuthStrategy { }
*/
class NoAuth extends BaseAuthStrategy {}
module.exports = NoAuth;

View file

@ -25,35 +25,59 @@ const BaseAuthStrategy = require('./BaseAuthStrategy');
* @param {number} options.rmMaxRetries - Sets the maximum number of retries for removing the session directory
*/
class RemoteAuth extends BaseAuthStrategy {
constructor({ clientId, dataPath, store, backupSyncIntervalMs, rmMaxRetries } = {}) {
if (!fs && !unzipper && !archiver) throw new Error('Optional Dependencies [fs-extra, unzipper, archiver] are required to use RemoteAuth. Make sure to run npm install correctly and remove the --no-optional flag');
constructor({
clientId,
dataPath,
store,
backupSyncIntervalMs,
rmMaxRetries,
} = {}) {
if (!fs && !unzipper && !archiver)
throw new Error(
'Optional Dependencies [fs-extra, unzipper, archiver] are required to use RemoteAuth. Make sure to run npm install correctly and remove the --no-optional flag',
);
super();
const idRegex = /^[-_\w]+$/i;
if (clientId && !idRegex.test(clientId)) {
throw new Error('Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.');
throw new Error(
'Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.',
);
}
if (!backupSyncIntervalMs || backupSyncIntervalMs < 60000) {
throw new Error('Invalid backupSyncIntervalMs. Accepts values starting from 60000ms {1 minute}.');
throw new Error(
'Invalid backupSyncIntervalMs. Accepts values starting from 60000ms {1 minute}.',
);
}
if(!store) throw new Error('Remote database store is required.');
if (!store) throw new Error('Remote database store is required.');
this.store = store;
this.clientId = clientId;
this.backupSyncIntervalMs = backupSyncIntervalMs;
this.dataPath = path.resolve(dataPath || './.wwebjs_auth/');
this.tempDir = `${this.dataPath}/wwebjs_temp_session_${this.clientId}`;
this.requiredDirs = ['Default', 'IndexedDB', 'Local Storage']; /* => Required Files & Dirs in WWebJS to restore session */
this.requiredDirs = [
'Default',
'IndexedDB',
'Local Storage',
]; /* => Required Files & Dirs in WWebJS to restore session */
this.rmMaxRetries = rmMaxRetries ?? 4;
}
async beforeBrowserInitialized() {
const puppeteerOpts = this.client.options.puppeteer;
const sessionDirName = this.clientId ? `RemoteAuth-${this.clientId}` : 'RemoteAuth';
const sessionDirName = this.clientId
? `RemoteAuth-${this.clientId}`
: 'RemoteAuth';
const dirPath = path.join(this.dataPath, sessionDirName);
if (puppeteerOpts.userDataDir && puppeteerOpts.userDataDir !== dirPath) {
throw new Error('RemoteAuth is not compatible with a user-supplied userDataDir.');
if (
puppeteerOpts.userDataDir &&
puppeteerOpts.userDataDir !== dirPath
) {
throw new Error(
'RemoteAuth is not compatible with a user-supplied userDataDir.',
);
}
this.userDataDir = dirPath;
@ -63,7 +87,7 @@ class RemoteAuth extends BaseAuthStrategy {
this.client.options.puppeteer = {
...puppeteerOpts,
userDataDir: dirPath
userDataDir: dirPath,
};
}
@ -80,20 +104,26 @@ class RemoteAuth extends BaseAuthStrategy {
let pathExists = await this.isValidPath(this.userDataDir);
if (pathExists) {
await fs.promises.rm(this.userDataDir, {
recursive: true,
force: true,
maxRetries: this.rmMaxRetries,
}).catch(() => {});
await fs.promises
.rm(this.userDataDir, {
recursive: true,
force: true,
maxRetries: this.rmMaxRetries,
})
.catch(() => {});
}
clearInterval(this.backupSync);
}
async afterAuthReady() {
const sessionExists = await this.store.sessionExists({session: this.sessionName});
if(!sessionExists) {
await this.delay(60000); /* Initial delay sync required for session to be stable enough to recover */
await this.storeRemoteSession({emit: true});
const sessionExists = await this.store.sessionExists({
session: this.sessionName,
});
if (!sessionExists) {
await this.delay(
60000,
); /* Initial delay sync required for session to be stable enough to recover */
await this.storeRemoteSession({ emit: true });
}
var self = this;
this.backupSync = setInterval(async function () {
@ -108,12 +138,15 @@ class RemoteAuth extends BaseAuthStrategy {
let compressedSessionPath;
try {
compressedSessionPath = await this.compressSession();
await this.store.save({ session: path.join(this.dataPath, this.sessionName) });
if(options && options.emit) this.client.emit(Events.REMOTE_SESSION_SAVED);
await this.store.save({
session: path.join(this.dataPath, this.sessionName),
});
if (options && options.emit)
this.client.emit(Events.REMOTE_SESSION_SAVED);
} finally {
const paths = [
this.tempDir,
...(compressedSessionPath ? [compressedSessionPath] : [])
...(compressedSessionPath ? [compressedSessionPath] : []),
];
await Promise.allSettled(
paths.map((p) =>
@ -121,25 +154,35 @@ class RemoteAuth extends BaseAuthStrategy {
recursive: true,
force: true,
maxRetries: this.rmMaxRetries,
})
)
}),
),
);
}
}
async extractRemoteSession() {
const pathExists = await this.isValidPath(this.userDataDir);
const compressedSessionPath = path.join(this.dataPath, `${this.sessionName}.zip`);
const sessionExists = await this.store.sessionExists({session: this.sessionName});
const compressedSessionPath = path.join(
this.dataPath,
`${this.sessionName}.zip`,
);
const sessionExists = await this.store.sessionExists({
session: this.sessionName,
});
if (pathExists) {
await fs.promises.rm(this.userDataDir, {
recursive: true,
force: true,
maxRetries: this.rmMaxRetries,
}).catch(() => {});
await fs.promises
.rm(this.userDataDir, {
recursive: true,
force: true,
maxRetries: this.rmMaxRetries,
})
.catch(() => {});
}
if (sessionExists) {
await this.store.extract({session: this.sessionName, path: compressedSessionPath});
await this.store.extract({
session: this.sessionName,
path: compressedSessionPath,
});
await this.unCompressSession(compressedSessionPath);
} else {
fs.mkdirSync(this.userDataDir, { recursive: true });
@ -147,8 +190,11 @@ class RemoteAuth extends BaseAuthStrategy {
}
async deleteRemoteSession() {
const sessionExists = await this.store.sessionExists({session: this.sessionName});
if (sessionExists) await this.store.delete({session: this.sessionName});
const sessionExists = await this.store.sessionExists({
session: this.sessionName,
});
if (sessionExists)
await this.store.delete({ session: this.sessionName });
}
async compressSession() {
@ -177,11 +223,14 @@ class RemoteAuth extends BaseAuthStrategy {
async unCompressSession(compressedSessionPath) {
var stream = fs.createReadStream(compressedSessionPath);
await new Promise((resolve, reject) => {
stream.pipe(unzipper.Extract({
path: this.userDataDir,
concurrency: 10
}))
.on('error', err => reject(err))
stream
.pipe(
unzipper.Extract({
path: this.userDataDir,
concurrency: 10,
}),
)
.on('error', (err) => reject(err))
.on('finish', () => resolve());
});
await fs.promises.unlink(compressedSessionPath);
@ -195,7 +244,7 @@ class RemoteAuth extends BaseAuthStrategy {
await fs.promises.cp(src, dest, {
recursive: true,
force: true,
errorOnExist: false
errorOnExist: false,
});
}
}
@ -211,7 +260,7 @@ class RemoteAuth extends BaseAuthStrategy {
}
async delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
}

View file

@ -5,7 +5,7 @@ const BusinessContact = require('../structures/BusinessContact');
class ContactFactory {
static create(client, data) {
if(data.isBusiness) {
if (data.isBusiness) {
return new BusinessContact(client, data);
}

View file

@ -16,7 +16,9 @@ class Base {
return Object.assign(Object.create(this), this);
}
_patch(data) { return data; }
_patch(data) {
return data;
}
}
module.exports = Base;

View file

@ -43,7 +43,7 @@ class Broadcast extends Base {
* Messages statuses
* @type {Message[]}
*/
this.msgs = data.msgs?.map(msg => new Message(this.client, msg));
this.msgs = data.msgs?.map((msg) => new Message(this.client, msg));
return super._patch(data);
}
@ -63,7 +63,6 @@ class Broadcast extends Base {
getContact() {
return this.client.getContactById(this.id._serialized);
}
}
module.exports = Broadcast;

View file

@ -15,7 +15,6 @@ class BusinessContact extends Contact {
return super._patch(data);
}
}
module.exports = BusinessContact;

View file

@ -49,7 +49,7 @@ class Buttons {
if (body instanceof MessageMedia) {
this.type = 'media';
this.title = '';
}else{
} else {
this.type = 'chat';
}
@ -58,8 +58,9 @@ class Buttons {
* @type {FormattedButtonSpec[]}
*/
this.buttons = this._format(buttons);
if(!this.buttons.length){ throw '[BT01] No buttons';}
if (!this.buttons.length) {
throw '[BT01] No buttons';
}
}
/**
@ -70,13 +71,16 @@ class Buttons {
* Input: [{id:'customId',body:'button1'},{body:'button2'},{body:'button3'},{body:'button4'}]
* Returns: [{ buttonId:'customId',buttonText:{'displayText':'button1'},type: 1 },{buttonId:'n3XKsL',buttonText:{'displayText':'button2'},type:1},{buttonId:'NDJk0a',buttonText:{'displayText':'button3'},type:1}]
*/
_format(buttons){
buttons = buttons.slice(0,3); // phone users can only see 3 buttons, so lets limit this
_format(buttons) {
buttons = buttons.slice(0, 3); // phone users can only see 3 buttons, so lets limit this
return buttons.map((btn) => {
return {'buttonId':btn.id ? String(btn.id) : Util.generateHash(6),'buttonText':{'displayText':btn.body},'type':1};
return {
buttonId: btn.id ? String(btn.id) : Util.generateHash(6),
buttonText: { displayText: btn.body },
type: 1,
};
});
}
}
module.exports = Buttons;

View file

@ -65,11 +65,15 @@ class Call extends Base {
/**
* Reject the call
*/
*/
async reject() {
return this.client.pupPage.evaluate((peerJid, id) => {
return window.WWebJS.rejectCall(peerJid, id);
}, this.from, this.id);
return this.client.pupPage.evaluate(
(peerJid, id) => {
return window.WWebJS.rejectCall(peerJid, id);
},
this.from,
this.id,
);
}
}

View file

@ -89,7 +89,9 @@ class Channel extends Base {
* Last message in the channel
* @type {Message}
*/
this.lastMessage = data.lastMessage ? new Message(super.client, data.lastMessage) : undefined;
this.lastMessage = data.lastMessage
? new Message(super.client, data.lastMessage)
: undefined;
return super._patch(data);
}
@ -100,17 +102,32 @@ class Channel extends Base {
* @returns {Promise<Array<{contact: Contact, role: string}>>} Returns an array of objects that handle the subscribed contacts and their roles in the channel
*/
async getSubscribers(limit) {
return await this.client.pupPage.evaluate(async (channelId, limit) => {
const channel = await window.WWebJS.getChat(channelId, { getAsModel: false });
if (!channel) return [];
!limit && (limit = window.require('WAWebNewsletterGatingUtils').getMaxSubscriberNumber());
const response = await (window.require('WAWebMexFetchNewsletterSubscribersJob')).mexFetchNewsletterSubscribers(channelId, limit);
const contacts = (window.require('WAWebNewsletterSubscriberListAction')).getSubscribersInContacts(response.subscribers);
return Promise.all(contacts.map((obj) => ({
...obj,
contact: window.WWebJS.getContactModel(obj.contact)
})));
}, this.id._serialized, limit);
return await this.client.pupPage.evaluate(
async (channelId, limit) => {
const channel = await window.WWebJS.getChat(channelId, {
getAsModel: false,
});
if (!channel) return [];
!limit &&
(limit = window
.require('WAWebNewsletterGatingUtils')
.getMaxSubscriberNumber());
const response = await window
.require('WAWebMexFetchNewsletterSubscribersJob')
.mexFetchNewsletterSubscribers(channelId, limit);
const contacts = window
.require('WAWebNewsletterSubscriberListAction')
.getSubscribersInContacts(response.subscribers);
return Promise.all(
contacts.map((obj) => ({
...obj,
contact: window.WWebJS.getContactModel(obj.contact),
})),
);
},
this.id._serialized,
limit,
);
}
/**
@ -119,7 +136,10 @@ class Channel extends Base {
* @returns {Promise<boolean>} Returns true if the subject was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setSubject(newSubject) {
const success = await this._setChannelMetadata({ name: newSubject }, { editName: true });
const success = await this._setChannelMetadata(
{ name: newSubject },
{ editName: true },
);
success && (this.name = newSubject);
return success;
}
@ -130,7 +150,10 @@ class Channel extends Base {
* @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
*/
async setDescription(newDescription) {
const success = await this._setChannelMetadata({ description: newDescription }, { editDescription: true });
const success = await this._setChannelMetadata(
{ description: newDescription },
{ editDescription: true },
);
success && (this.description = newDescription);
return success;
}
@ -141,7 +164,10 @@ class Channel extends Base {
* @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
*/
async setProfilePicture(newProfilePicture) {
return await this._setChannelMetadata({ picture: newProfilePicture }, { editPicture: true });
return await this._setChannelMetadata(
{ picture: newProfilePicture },
{ editPicture: true },
);
}
/**
@ -159,11 +185,11 @@ class Channel extends Base {
const reactionMapper = {
0: 3,
1: 1,
2: 0
2: 0,
};
const success = await this._setChannelMetadata(
{ reactionCodesSetting: reactionMapper[reactionCode] },
{ editReactionCodesSetting: true }
{ editReactionCodesSetting: true },
);
success && (this.channelMetadata.reactionCodesSetting = reactionCode);
return success;
@ -233,7 +259,11 @@ class Channel extends Base {
* @returns {Promise<boolean>} Returns true if an invitation was sent successfully, false otherwise
*/
async sendChannelAdminInvite(chatId, options = {}) {
return this.client.sendChannelAdminInvite(chatId, this.id._serialized, options);
return this.client.sendChannelAdminInvite(
chatId,
this.id._serialized,
options,
);
}
/**
@ -250,7 +280,10 @@ class Channel extends Base {
* @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
*/
async revokeChannelAdminInvite(userId) {
return this.client.revokeChannelAdminInvite(this.id._serialized, userId);
return this.client.revokeChannelAdminInvite(
this.id._serialized,
userId,
);
}
/**
@ -276,7 +309,11 @@ class Channel extends Base {
* @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
*/
async transferChannelOwnership(newOwnerId, options = {}) {
return this.client.transferChannelOwnership(this.id._serialized, newOwnerId, options);
return this.client.transferChannelOwnership(
this.id._serialized,
newOwnerId,
options,
);
}
/**
@ -287,36 +324,50 @@ class Channel extends Base {
* @returns {Promise<Array<Message>>}
*/
async fetchMessages(searchOptions) {
let messages = await this.client.pupPage.evaluate(async (channelId, searchOptions) => {
const msgFilter = (m) => {
if (m.isNotification || m.type === 'newsletter_notification') {
return false; // dont include notification messages
}
if (searchOptions && searchOptions.fromMe !== undefined && m.id.fromMe !== searchOptions.fromMe) {
return false;
}
return true;
};
let messages = await this.client.pupPage.evaluate(
async (channelId, searchOptions) => {
const msgFilter = (m) => {
if (
m.isNotification ||
m.type === 'newsletter_notification'
) {
return false; // dont include notification messages
}
if (
searchOptions &&
searchOptions.fromMe !== undefined &&
m.id.fromMe !== searchOptions.fromMe
) {
return false;
}
return true;
};
const channel = await window.WWebJS.getChat(channelId, { getAsModel: false });
let msgs = channel.msgs.getModelsArray().filter(msgFilter);
const channel = await window.WWebJS.getChat(channelId, {
getAsModel: false,
});
let msgs = channel.msgs.getModelsArray().filter(msgFilter);
if (searchOptions && searchOptions.limit > 0) {
while (msgs.length < searchOptions.limit) {
const loadedMessages = await (window.require('WAWebChatLoadMessages')).loadEarlierMsgs(channel);
if (!loadedMessages || !loadedMessages.length) break;
msgs = [...loadedMessages.filter(msgFilter), ...msgs];
if (searchOptions && searchOptions.limit > 0) {
while (msgs.length < searchOptions.limit) {
const loadedMessages = await window
.require('WAWebChatLoadMessages')
.loadEarlierMsgs(channel);
if (!loadedMessages || !loadedMessages.length) break;
msgs = [...loadedMessages.filter(msgFilter), ...msgs];
}
if (msgs.length > searchOptions.limit) {
msgs.sort((a, b) => (a.t > b.t ? 1 : -1));
msgs = msgs.splice(msgs.length - searchOptions.limit);
}
}
if (msgs.length > searchOptions.limit) {
msgs.sort((a, b) => (a.t > b.t) ? 1 : -1);
msgs = msgs.splice(msgs.length - searchOptions.limit);
}
}
return msgs.map(m => window.WWebJS.getMessageModel(m));
}, this.id._serialized, searchOptions);
return msgs.map((m) => window.WWebJS.getMessageModel(m));
},
this.id._serialized,
searchOptions,
);
return messages.map((msg) => new Message(this.client, msg));
}
@ -336,27 +387,39 @@ class Channel extends Base {
* @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
*/
async _setChannelMetadata(value, property) {
return await this.client.pupPage.evaluate(async (channelId, value, property) => {
const channel = await window.WWebJS.getChat(channelId, { getAsModel: false });
if (!channel) return false;
if (property.editPicture) {
value.picture = value.picture
? await window.WWebJS.cropAndResizeImage(value.picture, {
asDataUrl: true,
mimetype: 'image/jpeg',
size: 640,
quality: 1
})
: null;
}
try {
await (window.require('WAWebEditNewsletterMetadataAction')).editNewsletterMetadataAction(channel, property, value);
return true;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
}, this.id._serialized, value, property);
return await this.client.pupPage.evaluate(
async (channelId, value, property) => {
const channel = await window.WWebJS.getChat(channelId, {
getAsModel: false,
});
if (!channel) return false;
if (property.editPicture) {
value.picture = value.picture
? await window.WWebJS.cropAndResizeImage(
value.picture,
{
asDataUrl: true,
mimetype: 'image/jpeg',
size: 640,
quality: 1,
},
)
: null;
}
try {
await window
.require('WAWebEditNewsletterMetadataAction')
.editNewsletterMetadataAction(channel, property, value);
return true;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
},
this.id._serialized,
value,
property,
);
}
/**
@ -365,19 +428,35 @@ class Channel extends Base {
* @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
*/
async _muteUnmuteChannel(action) {
return await this.client.pupPage.evaluate(async (channelId, action) => {
try {
await (window.require('WAWebNewsletterUpdateUserSettingJob')).updateNewsletterUserSetting({
newsletterJid: window.require('WAJids').toNewsletterJid(channelId),
type: window.require('WAWebNewsletterModelUtils').ADMIN_NOTIFICATIONS,
muteExpirationValue: action === 'MUTE' ? window.require('WAWebNewsletterModelUtils').MUTED_STATE : window.require('WAWebNewsletterModelUtils').UNMUTED_STATE
});
return true;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
}, this.id._serialized, action);
return await this.client.pupPage.evaluate(
async (channelId, action) => {
try {
await window
.require('WAWebNewsletterUpdateUserSettingJob')
.updateNewsletterUserSetting({
newsletterJid: window
.require('WAJids')
.toNewsletterJid(channelId),
type: window.require('WAWebNewsletterModelUtils')
.ADMIN_NOTIFICATIONS,
muteExpirationValue:
action === 'MUTE'
? window.require(
'WAWebNewsletterModelUtils',
).MUTED_STATE
: window.require(
'WAWebNewsletterModelUtils',
).UNMUTED_STATE,
});
return true;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
},
this.id._serialized,
action,
);
}
}

View file

@ -85,7 +85,9 @@ class Chat extends Base {
* Last message fo chat
* @type {Message}
*/
this.lastMessage = data.lastMessage ? new Message(this.client, data.lastMessage) : undefined;
this.lastMessage = data.lastMessage
? new Message(this.client, data.lastMessage)
: undefined;
return super._patch(data);
}
@ -113,7 +115,7 @@ class Chat extends Base {
* @returns {Promise<boolean>} result
*/
async clearMessages() {
return this.client.pupPage.evaluate(chatId => {
return this.client.pupPage.evaluate((chatId) => {
return window.WWebJS.sendClearChat(chatId);
}, this.id._serialized);
}
@ -123,7 +125,7 @@ class Chat extends Base {
* @returns {Promise<Boolean>} result
*/
async delete() {
return this.client.pupPage.evaluate(chatId => {
return this.client.pupPage.evaluate((chatId) => {
return window.WWebJS.sendDeleteChat(chatId);
}, this.id._serialized);
}
@ -164,7 +166,10 @@ class Chat extends Base {
* @returns {Promise<{isMuted: boolean, muteExpiration: number}>}
*/
async mute(unmuteDate) {
const result = await this.client.muteChat(this.id._serialized, unmuteDate);
const result = await this.client.muteChat(
this.id._serialized,
unmuteDate,
);
this.isMuted = result.isMuted;
this.muteExpiration = result.muteExpiration;
return result;
@ -184,7 +189,7 @@ class Chat extends Base {
/**
* Mark this chat as unread
*/
async markUnread(){
async markUnread() {
return this.client.markChatUnread(this.id._serialized);
}
@ -196,45 +201,56 @@ class Chat extends Base {
* @returns {Promise<Array<Message>>}
*/
async fetchMessages(searchOptions) {
let messages = await this.client.pupPage.evaluate(async (chatId, searchOptions) => {
const msgFilter = (m) => {
if (m.isNotification) {
return false; // dont include notification messages
}
if (searchOptions && searchOptions.fromMe !== undefined && m.id.fromMe !== searchOptions.fromMe) {
return false;
}
return true;
};
let messages = await this.client.pupPage.evaluate(
async (chatId, searchOptions) => {
const msgFilter = (m) => {
if (m.isNotification) {
return false; // dont include notification messages
}
if (
searchOptions &&
searchOptions.fromMe !== undefined &&
m.id.fromMe !== searchOptions.fromMe
) {
return false;
}
return true;
};
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
let msgs = chat.msgs.getModelsArray().filter(msgFilter);
const chat = await window.WWebJS.getChat(chatId, {
getAsModel: false,
});
let msgs = chat.msgs.getModelsArray().filter(msgFilter);
if (searchOptions && searchOptions.limit > 0) {
while (msgs.length < searchOptions.limit) {
const loadedMessages = await (window.require('WAWebChatLoadMessages')).loadEarlierMsgs(chat,chat.msgs);
if (!loadedMessages || !loadedMessages.length) break;
msgs = [...loadedMessages.filter(msgFilter), ...msgs];
if (searchOptions && searchOptions.limit > 0) {
while (msgs.length < searchOptions.limit) {
const loadedMessages = await window
.require('WAWebChatLoadMessages')
.loadEarlierMsgs(chat, chat.msgs);
if (!loadedMessages || !loadedMessages.length) break;
msgs = [...loadedMessages.filter(msgFilter), ...msgs];
}
if (msgs.length > searchOptions.limit) {
msgs.sort((a, b) => (a.t > b.t ? 1 : -1));
msgs = msgs.splice(msgs.length - searchOptions.limit);
}
}
if (msgs.length > searchOptions.limit) {
msgs.sort((a, b) => (a.t > b.t) ? 1 : -1);
msgs = msgs.splice(msgs.length - searchOptions.limit);
}
}
return msgs.map((m) => window.WWebJS.getMessageModel(m));
},
this.id._serialized,
searchOptions,
);
return msgs.map(m => window.WWebJS.getMessageModel(m));
}, this.id._serialized, searchOptions);
return messages.map(m => new Message(this.client, m));
return messages.map((m) => new Message(this.client, m));
}
/**
* Simulate typing in chat. This will last for 25 seconds.
*/
async sendStateTyping() {
return this.client.pupPage.evaluate(chatId => {
return this.client.pupPage.evaluate((chatId) => {
window.WWebJS.sendChatstate('typing', chatId);
return true;
}, this.id._serialized);
@ -244,7 +260,7 @@ class Chat extends Base {
* Simulate recording audio in chat. This will last for 25 seconds.
*/
async sendStateRecording() {
return this.client.pupPage.evaluate(chatId => {
return this.client.pupPage.evaluate((chatId) => {
window.WWebJS.sendChatstate('recording', chatId);
return true;
}, this.id._serialized);
@ -254,7 +270,7 @@ class Chat extends Base {
* Stops typing or recording in chat immediately.
*/
async clearState() {
return this.client.pupPage.evaluate(chatId => {
return this.client.pupPage.evaluate((chatId) => {
window.WWebJS.sendChatstate('stop', chatId);
return true;
}, this.id._serialized);

View file

@ -18,7 +18,7 @@ class Contact extends Base {
constructor(client, data) {
super(client);
if(data) this._patch(data);
if (data) this._patch(data);
}
_patch(data) {
@ -142,7 +142,7 @@ class Contact extends Base {
* @returns {Promise<Chat>}
*/
async getChat() {
if(this.isMe) return null;
if (this.isMe) return null;
return await this.client.getChatById(this.id._serialized);
}
@ -152,11 +152,13 @@ class Contact extends Base {
* @returns {Promise<boolean>}
*/
async block() {
if(this.isGroup) return false;
if (this.isGroup) return false;
await this.client.pupPage.evaluate(async (contactId) => {
const chat = await window.WWebJS.getChat(contactId);
await (window.require('WAWebBlockContactAction')).blockContact({'contact':chat});
await window
.require('WAWebBlockContactAction')
.blockContact({ contact: chat });
}, this.id._serialized);
this.isBlocked = true;
@ -168,11 +170,15 @@ class Contact extends Base {
* @returns {Promise<boolean>}
*/
async unblock() {
if(this.isGroup) return false;
if (this.isGroup) return false;
await this.client.pupPage.evaluate(async (contactId) => {
const contact = (window.require('WAWebCollections')).Contact.get(contactId);
await (window.require('WAWebBlockContactAction')).unblockContact(contact);
const contact = window
.require('WAWebCollections')
.Contact.get(contactId);
await window
.require('WAWebBlockContactAction')
.unblockContact(contact);
}, this.id._serialized);
this.isBlocked = false;
@ -186,11 +192,12 @@ class Contact extends Base {
async getAbout() {
const about = await this.client.pupPage.evaluate(async (contactId) => {
const wid = window.require('WAWebWidFactory').createWid(contactId);
return (window.require('WAWebContactStatusBridge')).getStatus({'token':'', 'wid': wid});
return window
.require('WAWebContactStatusBridge')
.getStatus({ token: '', wid: wid });
}, this.id._serialized);
if (typeof about.status !== 'string')
return null;
if (typeof about.status !== 'string') return null;
return about.status;
}
@ -206,7 +213,7 @@ class Contact extends Base {
/**
* Gets the Contact's current status broadcast.
* @returns {Promise<Broadcast>}
*/
*/
async getBroadcast() {
return await this.client.getBroadcastById(this.id._serialized);
}

View file

@ -76,115 +76,183 @@ class GroupChat extends Chat {
* @returns {Promise<Object.<string, AddParticipantsResult>|string>} Returns an object with the resulting data or an error message as a string
*/
async addParticipants(participantIds, options = {}) {
return await this.client.pupPage.evaluate(async (groupId, participantIds, options) => {
const { sleep = [250, 500], autoSendInviteV4 = true, comment = '' } = options;
const participantData = {};
return await this.client.pupPage.evaluate(
async (groupId, participantIds, options) => {
const {
sleep = [250, 500],
autoSendInviteV4 = true,
comment = '',
} = options;
const participantData = {};
!Array.isArray(participantIds) && (participantIds = [participantIds]);
const groupWid = window.require('WAWebWidFactory').createWid(groupId);
const group = (window.require('WAWebCollections')).Chat.get(groupWid) || (await (window.require('WAWebCollections')).Chat.find(groupWid));
const participantWids = participantIds.map((p) => window.require('WAWebWidFactory').createWid(p));
!Array.isArray(participantIds) &&
(participantIds = [participantIds]);
const groupWid = window
.require('WAWebWidFactory')
.createWid(groupId);
const group =
window.require('WAWebCollections').Chat.get(groupWid) ||
(await window
.require('WAWebCollections')
.Chat.find(groupWid));
const participantWids = participantIds.map((p) =>
window.require('WAWebWidFactory').createWid(p),
);
const errorCodes = {
default: 'An unknown error occupied while adding a participant',
isGroupEmpty: 'AddParticipantsError: The participant can\'t be added to an empty group',
iAmNotAdmin: 'AddParticipantsError: You have no admin rights to add a participant to a group',
200: 'The participant was added successfully',
403: 'The participant can be added by sending private invitation only',
404: 'The phone number is not registered on WhatsApp',
408: 'You cannot add this participant because they recently left the group',
409: 'The participant is already a group member',
417: 'The participant can\'t be added to the community. You can invite them privately to join this group through its invite link',
419: 'The participant can\'t be added because the group is full'
};
await (window.require('WAWebGroupQueryJob')).queryAndUpdateGroupMetadataById({ id: groupId });
let groupParticipants = group.groupMetadata?.participants.serialize();
if (!groupParticipants) {
return errorCodes.isGroupEmpty;
}
if (!group.iAmAdmin()) {
return errorCodes.iAmNotAdmin;
}
groupParticipants.map(({ id }) => {
return id.server === 'lid' ? window.require('WAWebApiContact').getPhoneNumber(id) : id;
});
const _getSleepTime = (sleep) => {
if (!Array.isArray(sleep) || sleep.length === 2 && sleep[0] === sleep[1]) {
return sleep;
}
if (sleep.length === 1) {
return sleep[0];
}
(sleep[1] - sleep[0]) < 100 && (sleep[0] = sleep[1]) && (sleep[1] += 100);
return Math.floor(Math.random() * (sleep[1] - sleep[0] + 1)) + sleep[0];
};
for (let pWid of participantWids) {
const pId = pWid._serialized;
pWid = pWid.server === 'lid' ? window.require('WAWebApiContact').getPhoneNumber(pWid) : pWid;
participantData[pId] = {
code: undefined,
message: undefined,
isInviteV4Sent: false
const errorCodes = {
default:
'An unknown error occupied while adding a participant',
isGroupEmpty:
"AddParticipantsError: The participant can't be added to an empty group",
iAmNotAdmin:
'AddParticipantsError: You have no admin rights to add a participant to a group',
200: 'The participant was added successfully',
403: 'The participant can be added by sending private invitation only',
404: 'The phone number is not registered on WhatsApp',
408: 'You cannot add this participant because they recently left the group',
409: 'The participant is already a group member',
417: "The participant can't be added to the community. You can invite them privately to join this group through its invite link",
419: "The participant can't be added because the group is full",
};
if (groupParticipants.some(p => p._serialized === pId)) {
participantData[pId].code = 409;
participantData[pId].message = errorCodes[409];
continue;
await window
.require('WAWebGroupQueryJob')
.queryAndUpdateGroupMetadataById({ id: groupId });
let groupParticipants =
group.groupMetadata?.participants.serialize();
if (!groupParticipants) {
return errorCodes.isGroupEmpty;
}
if (!(await (window.require('WAWebQueryExistsJob').queryWidExists)(pWid))?.wid) {
participantData[pId].code = 404;
participantData[pId].message = errorCodes[404];
continue;
if (!group.iAmAdmin()) {
return errorCodes.iAmNotAdmin;
}
const rpcResult =
await window.WWebJS.getAddParticipantsRpcResult(groupWid, pWid);
const { code: rpcResultCode } = rpcResult;
groupParticipants.map(({ id }) => {
return id.server === 'lid'
? window.require('WAWebApiContact').getPhoneNumber(id)
: id;
});
participantData[pId].code = rpcResultCode;
participantData[pId].message =
errorCodes[rpcResultCode] || errorCodes.default;
const _getSleepTime = (sleep) => {
if (
!Array.isArray(sleep) ||
(sleep.length === 2 && sleep[0] === sleep[1])
) {
return sleep;
}
if (sleep.length === 1) {
return sleep[0];
}
sleep[1] - sleep[0] < 100 &&
(sleep[0] = sleep[1]) &&
(sleep[1] += 100);
return (
Math.floor(Math.random() * (sleep[1] - sleep[0] + 1)) +
sleep[0]
);
};
if (autoSendInviteV4 && rpcResultCode === 403) {
let userChat, isInviteV4Sent = false;
(window.require('WAWebCollections')).Contact.gadd(pWid, { silent: true });
for (let pWid of participantWids) {
const pId = pWid._serialized;
pWid =
pWid.server === 'lid'
? window
.require('WAWebApiContact')
.getPhoneNumber(pWid)
: pWid;
if (rpcResult.name === 'ParticipantRequestCodeCanBeSent' &&
(userChat = (window.require('WAWebCollections')).Chat.get(pWid) || (await (window.require('WAWebCollections')).Chat.find(pWid)))) {
const groupName = group.formattedTitle || group.name;
const res = await (window.require('WAWebChatSendMessages')).sendGroupInviteMessage(
userChat,
group.id._serialized,
groupName,
rpcResult.inviteV4Code,
rpcResult.inviteV4CodeExp,
comment,
await window.WWebJS.getProfilePicThumbToBase64(groupWid)
);
isInviteV4Sent = res.messageSendResult === 'OK';
participantData[pId] = {
code: undefined,
message: undefined,
isInviteV4Sent: false,
};
if (groupParticipants.some((p) => p._serialized === pId)) {
participantData[pId].code = 409;
participantData[pId].message = errorCodes[409];
continue;
}
participantData[pId].isInviteV4Sent = isInviteV4Sent;
if (
!(
await window
.require('WAWebQueryExistsJob')
.queryWidExists(pWid)
)?.wid
) {
participantData[pId].code = 404;
participantData[pId].message = errorCodes[404];
continue;
}
const rpcResult =
await window.WWebJS.getAddParticipantsRpcResult(
groupWid,
pWid,
);
const { code: rpcResultCode } = rpcResult;
participantData[pId].code = rpcResultCode;
participantData[pId].message =
errorCodes[rpcResultCode] || errorCodes.default;
if (autoSendInviteV4 && rpcResultCode === 403) {
let userChat,
isInviteV4Sent = false;
window
.require('WAWebCollections')
.Contact.gadd(pWid, { silent: true });
if (
rpcResult.name ===
'ParticipantRequestCodeCanBeSent' &&
(userChat =
window
.require('WAWebCollections')
.Chat.get(pWid) ||
(await window
.require('WAWebCollections')
.Chat.find(pWid)))
) {
const groupName =
group.formattedTitle || group.name;
const res = await window
.require('WAWebChatSendMessages')
.sendGroupInviteMessage(
userChat,
group.id._serialized,
groupName,
rpcResult.inviteV4Code,
rpcResult.inviteV4CodeExp,
comment,
await window.WWebJS.getProfilePicThumbToBase64(
groupWid,
),
);
isInviteV4Sent = res.messageSendResult === 'OK';
}
participantData[pId].isInviteV4Sent = isInviteV4Sent;
}
sleep &&
participantWids.length > 1 &&
participantWids.indexOf(pWid) !==
participantWids.length - 1 &&
(await new Promise((resolve) =>
setTimeout(resolve, _getSleepTime(sleep)),
));
}
sleep &&
participantWids.length > 1 &&
participantWids.indexOf(pWid) !== participantWids.length - 1 &&
(await new Promise((resolve) => setTimeout(resolve, _getSleepTime(sleep))));
}
return participantData;
}, this.id._serialized, participantIds, options);
return participantData;
},
this.id._serialized,
participantIds,
options,
);
}
/**
@ -193,17 +261,36 @@ class GroupChat extends Chat {
* @returns {Promise<{ status: number }>}
*/
async removeParticipants(participantIds) {
return await this.client.pupPage.evaluate(async (chatId, participantIds) => {
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
const participants = (await Promise.all(participantIds.map(async p => {
const { lid, phone } = await window.WWebJS.enforceLidAndPnRetrieval(p);
return await this.client.pupPage.evaluate(
async (chatId, participantIds) => {
const chat = await window.WWebJS.getChat(chatId, {
getAsModel: false,
});
const participants = (
await Promise.all(
participantIds.map(async (p) => {
const { lid, phone } =
await window.WWebJS.enforceLidAndPnRetrieval(p);
return chat.groupMetadata.participants.get(lid?._serialized) ||
chat.groupMetadata.participants.get(phone?._serialized);
}))).filter(Boolean);
await (window.require('WAWebModifyParticipantsGroupAction')).removeParticipants(chat, participants);
return { status: 200 };
}, this.id._serialized, participantIds);
return (
chat.groupMetadata.participants.get(
lid?._serialized,
) ||
chat.groupMetadata.participants.get(
phone?._serialized,
)
);
}),
)
).filter(Boolean);
await window
.require('WAWebModifyParticipantsGroupAction')
.removeParticipants(chat, participants);
return { status: 200 };
},
this.id._serialized,
participantIds,
);
}
/**
@ -212,17 +299,36 @@ class GroupChat extends Chat {
* @returns {Promise<{ status: number }>} Object with status code indicating if the operation was successful
*/
async promoteParticipants(participantIds) {
return await this.client.pupPage.evaluate(async (chatId, participantIds) => {
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
const participants = (await Promise.all(participantIds.map(async p => {
const { lid, phone } = await window.WWebJS.enforceLidAndPnRetrieval(p);
return await this.client.pupPage.evaluate(
async (chatId, participantIds) => {
const chat = await window.WWebJS.getChat(chatId, {
getAsModel: false,
});
const participants = (
await Promise.all(
participantIds.map(async (p) => {
const { lid, phone } =
await window.WWebJS.enforceLidAndPnRetrieval(p);
return chat.groupMetadata.participants.get(lid?._serialized) ||
chat.groupMetadata.participants.get(phone?._serialized);
}))).filter(Boolean);
await (window.require('WAWebModifyParticipantsGroupAction')).promoteParticipants(chat, participants);
return { status: 200 };
}, this.id._serialized, participantIds);
return (
chat.groupMetadata.participants.get(
lid?._serialized,
) ||
chat.groupMetadata.participants.get(
phone?._serialized,
)
);
}),
)
).filter(Boolean);
await window
.require('WAWebModifyParticipantsGroupAction')
.promoteParticipants(chat, participants);
return { status: 200 };
},
this.id._serialized,
participantIds,
);
}
/**
@ -231,17 +337,36 @@ class GroupChat extends Chat {
* @returns {Promise<{ status: number }>} Object with status code indicating if the operation was successful
*/
async demoteParticipants(participantIds) {
return await this.client.pupPage.evaluate(async (chatId, participantIds) => {
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
const participants = (await Promise.all(participantIds.map(async p => {
const { lid, phone } = await window.WWebJS.enforceLidAndPnRetrieval(p);
return await this.client.pupPage.evaluate(
async (chatId, participantIds) => {
const chat = await window.WWebJS.getChat(chatId, {
getAsModel: false,
});
const participants = (
await Promise.all(
participantIds.map(async (p) => {
const { lid, phone } =
await window.WWebJS.enforceLidAndPnRetrieval(p);
return chat.groupMetadata.participants.get(lid?._serialized) ||
chat.groupMetadata.participants.get(phone?._serialized);
}))).filter(Boolean);
await (window.require('WAWebModifyParticipantsGroupAction')).demoteParticipants(chat, participants);
return { status: 200 };
}, this.id._serialized, participantIds);
return (
chat.groupMetadata.participants.get(
lid?._serialized,
) ||
chat.groupMetadata.participants.get(
phone?._serialized,
)
);
}),
)
).filter(Boolean);
await window
.require('WAWebModifyParticipantsGroupAction')
.demoteParticipants(chat, participants);
return { status: 200 };
},
this.id._serialized,
participantIds,
);
}
/**
@ -250,18 +375,26 @@ class GroupChat extends Chat {
* @returns {Promise<boolean>} Returns true if the subject was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setSubject(subject) {
const success = await this.client.pupPage.evaluate(async (chatId, subject) => {
const chatWid = window.require('WAWebWidFactory').createWid(chatId);
try {
await (window.require('WAWebGroupModifyInfoJob')).setGroupSubject(chatWid, subject);
return true;
} catch (err) {
if(err.name === 'ServerStatusCodeError') return false;
throw err;
}
}, this.id._serialized, subject);
const success = await this.client.pupPage.evaluate(
async (chatId, subject) => {
const chatWid = window
.require('WAWebWidFactory')
.createWid(chatId);
try {
await window
.require('WAWebGroupModifyInfoJob')
.setGroupSubject(chatWid, subject);
return true;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
},
this.id._serialized,
subject,
);
if(!success) return false;
if (!success) return false;
this.name = subject;
return true;
}
@ -272,21 +405,36 @@ class GroupChat extends Chat {
* @returns {Promise<boolean>} Returns true if the description was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setDescription(description) {
const success = await this.client.pupPage.evaluate(async (chatId, description) => {
const chatWid = window.require('WAWebWidFactory').createWid(chatId);
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
let descId = chat.groupMetadata.descId;
let newId = await (window.require('WAWebMsgKey')).newId();
try {
await (window.require('WAWebGroupModifyInfoJob')).setGroupDescription(chatWid, description, newId, descId);
return true;
} catch (err) {
if(err.name === 'ServerStatusCodeError') return false;
throw err;
}
}, this.id._serialized, description);
const success = await this.client.pupPage.evaluate(
async (chatId, description) => {
const chatWid = window
.require('WAWebWidFactory')
.createWid(chatId);
const chat = await window.WWebJS.getChat(chatId, {
getAsModel: false,
});
let descId = chat.groupMetadata.descId;
let newId = await window.require('WAWebMsgKey').newId();
try {
await window
.require('WAWebGroupModifyInfoJob')
.setGroupDescription(
chatWid,
description,
newId,
descId,
);
return true;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
},
this.id._serialized,
description,
);
if(!success) return false;
if (!success) return false;
this.groupMetadata.desc = description;
return true;
}
@ -296,19 +444,34 @@ class GroupChat extends Chat {
* @param {boolean} [adminsOnly=true] Enable or disable this option
* @returns {Promise<boolean>} Returns true if the setting was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setAddMembersAdminsOnly(adminsOnly=true) {
const success = await this.client.pupPage.evaluate(async (groupId, adminsOnly) => {
const chat = await window.WWebJS.getChat(groupId, { getAsModel: false });
try {
await (window.require('WAWebSetPropertyGroupAction')).setGroupProperty(chat, 'member_add_mode', adminsOnly ? 0 : 1);
return true;
} catch (err) {
if(err.name === 'ServerStatusCodeError') return false;
throw err;
}
}, this.id._serialized, adminsOnly);
async setAddMembersAdminsOnly(adminsOnly = true) {
const success = await this.client.pupPage.evaluate(
async (groupId, adminsOnly) => {
const chat = await window.WWebJS.getChat(groupId, {
getAsModel: false,
});
try {
await window
.require('WAWebSetPropertyGroupAction')
.setGroupProperty(
chat,
'member_add_mode',
adminsOnly ? 0 : 1,
);
return true;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
},
this.id._serialized,
adminsOnly,
);
success && (this.groupMetadata.memberAddMode = adminsOnly ? 'admin_add' : 'all_member_add');
success &&
(this.groupMetadata.memberAddMode = adminsOnly
? 'admin_add'
: 'all_member_add');
return success;
}
@ -317,19 +480,31 @@ class GroupChat extends Chat {
* @param {boolean} [adminsOnly=true] Enable or disable this option
* @returns {Promise<boolean>} Returns true if the setting was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setMessagesAdminsOnly(adminsOnly=true) {
const success = await this.client.pupPage.evaluate(async (chatId, adminsOnly) => {
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
try {
await (window.require('WAWebSetPropertyGroupAction')).setGroupProperty(chat, 'announcement', adminsOnly ? 1 : 0);
return true;
} catch (err) {
if(err.name === 'ServerStatusCodeError') return false;
throw err;
}
}, this.id._serialized, adminsOnly);
async setMessagesAdminsOnly(adminsOnly = true) {
const success = await this.client.pupPage.evaluate(
async (chatId, adminsOnly) => {
const chat = await window.WWebJS.getChat(chatId, {
getAsModel: false,
});
try {
await window
.require('WAWebSetPropertyGroupAction')
.setGroupProperty(
chat,
'announcement',
adminsOnly ? 1 : 0,
);
return true;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
},
this.id._serialized,
adminsOnly,
);
if(!success) return false;
if (!success) return false;
this.groupMetadata.announce = adminsOnly;
return true;
@ -340,19 +515,27 @@ class GroupChat extends Chat {
* @param {boolean} [adminsOnly=true] Enable or disable this option
* @returns {Promise<boolean>} Returns true if the setting was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setInfoAdminsOnly(adminsOnly=true) {
const success = await this.client.pupPage.evaluate(async (chatId, adminsOnly) => {
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
try {
await (window.require('WAWebSetPropertyGroupAction')).setGroupProperty(chat, 'restrict', adminsOnly ? 1 : 0);
return true;
} catch (err) {
if(err.name === 'ServerStatusCodeError') return false;
throw err;
}
}, this.id._serialized, adminsOnly);
async setInfoAdminsOnly(adminsOnly = true) {
const success = await this.client.pupPage.evaluate(
async (chatId, adminsOnly) => {
const chat = await window.WWebJS.getChat(chatId, {
getAsModel: false,
});
try {
await window
.require('WAWebSetPropertyGroupAction')
.setGroupProperty(chat, 'restrict', adminsOnly ? 1 : 0);
return true;
} catch (err) {
if (err.name === 'ServerStatusCodeError') return false;
throw err;
}
},
this.id._serialized,
adminsOnly,
);
if(!success) return false;
if (!success) return false;
this.groupMetadata.restrict = adminsOnly;
return true;
@ -376,9 +559,13 @@ class GroupChat extends Chat {
* @returns {Promise<boolean>} Returns true if the picture was properly updated. This can return false if the user does not have the necessary permissions.
*/
async setPicture(media) {
const success = await this.client.pupPage.evaluate((chatid, media) => {
return window.WWebJS.setPicture(chatid, media);
}, this.id._serialized, media);
const success = await this.client.pupPage.evaluate(
(chatid, media) => {
return window.WWebJS.setPicture(chatid, media);
},
this.id._serialized,
media,
);
return success;
}
@ -388,19 +575,18 @@ class GroupChat extends Chat {
* @returns {Promise<string>} Group's invite code
*/
async getInviteCode() {
const codeRes = await this.client.pupPage.evaluate(async chatId => {
const codeRes = await this.client.pupPage.evaluate(async (chatId) => {
try {
return await (window.require('WAWebMexFetchGroupInviteCodeJob')).fetchMexGroupInviteCode(chatId);
}
catch (err) {
if(err.name === 'ServerStatusCodeError') return undefined;
return await window
.require('WAWebMexFetchGroupInviteCodeJob')
.fetchMexGroupInviteCode(chatId);
} catch (err) {
if (err.name === 'ServerStatusCodeError') return undefined;
throw err;
}
}, this.id._serialized);
return codeRes?.code
? codeRes?.code
: codeRes;
return codeRes?.code ? codeRes?.code : codeRes;
}
/**
@ -408,9 +594,11 @@ class GroupChat extends Chat {
* @returns {Promise<string>} New invite code
*/
async revokeInvite() {
const codeRes = await this.client.pupPage.evaluate(chatId => {
const codeRes = await this.client.pupPage.evaluate((chatId) => {
const chatWid = window.require('WAWebWidFactory').createWid(chatId);
return (window.require('WAWebGroupQueryJob')).resetGroupInviteCode(chatWid);
return window
.require('WAWebGroupQueryJob')
.resetGroupInviteCode(chatWid);
}, this.id._serialized);
return codeRes.code;
@ -431,7 +619,9 @@ class GroupChat extends Chat {
* @returns {Promise<Array<GroupMembershipRequest>>} An array of membership requests
*/
async getGroupMembershipRequests() {
return await this.client.getGroupMembershipRequests(this.id._serialized);
return await this.client.getGroupMembershipRequests(
this.id._serialized,
);
}
/**
@ -455,7 +645,10 @@ class GroupChat extends Chat {
* @returns {Promise<Array<MembershipRequestActionResult>>} Returns an array of requester IDs whose membership requests were approved and an error for each requester, if any occurred during the operation. If there are no requests, an empty array will be returned
*/
async approveGroupMembershipRequests(options = {}) {
return await this.client.approveGroupMembershipRequests(this.id._serialized, options);
return await this.client.approveGroupMembershipRequests(
this.id._serialized,
options,
);
}
/**
@ -464,7 +657,10 @@ class GroupChat extends Chat {
* @returns {Promise<Array<MembershipRequestActionResult>>} Returns an array of requester IDs whose membership requests were rejected and an error for each requester, if any occurred during the operation. If there are no requests, an empty array will be returned
*/
async rejectGroupMembershipRequests(options = {}) {
return await this.client.rejectGroupMembershipRequests(this.id._serialized, options);
return await this.client.rejectGroupMembershipRequests(
this.id._serialized,
options,
);
}
/**
@ -472,12 +668,13 @@ class GroupChat extends Chat {
* @returns {Promise}
*/
async leave() {
await this.client.pupPage.evaluate(async chatId => {
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
return (window.require('WAWebExitGroupAction')).sendExitGroup(chat);
await this.client.pupPage.evaluate(async (chatId) => {
const chat = await window.WWebJS.getChat(chatId, {
getAsModel: false,
});
return window.require('WAWebExitGroupAction').sendExitGroup(chat);
}, this.id._serialized);
}
}
module.exports = GroupChat;

View file

@ -10,7 +10,7 @@ class GroupNotification extends Base {
constructor(client, data) {
super(client);
if(data) this._patch(data);
if (data) this._patch(data);
}
_patch(data) {
@ -43,13 +43,19 @@ class GroupNotification extends Base {
*
* @type {string}
*/
this.chatId = typeof (data.id.remote) === 'object' ? data.id.remote._serialized : data.id.remote;
this.chatId =
typeof data.id.remote === 'object'
? data.id.remote._serialized
: data.id.remote;
/**
* ContactId for the user that produced the GroupNotification.
* @type {string}
*/
this.author = typeof (data.author) === 'object' ? data.author._serialized : data.author;
this.author =
typeof data.author === 'object'
? data.author._serialized
: data.author;
/**
* Contact IDs for the users that were affected by this GroupNotification.
@ -85,7 +91,11 @@ class GroupNotification extends Base {
* @returns {Promise<Array<Contact>>}
*/
async getRecipients() {
return await Promise.all(this.recipientIds.map(async m => await this.client.getContactById(m)));
return await Promise.all(
this.recipientIds.map(
async (m) => await this.client.getContactById(m),
),
);
}
/**
@ -95,10 +105,9 @@ class GroupNotification extends Base {
* @param {object} options
* @returns {Promise<Message>}
*/
async reply(content, options={}) {
async reply(content, options = {}) {
return this.client.sendMessage(this.chatId, content, options);
}
}
module.exports = GroupNotification;

View file

@ -12,13 +12,13 @@ class Label extends Base {
* @param {Base} client
* @param {object} labelData
*/
constructor(client, labelData){
constructor(client, labelData) {
super(client);
if(labelData) this._patch(labelData);
if (labelData) this._patch(labelData);
}
_patch(labelData){
_patch(labelData) {
/**
* Label ID
* @type {string}
@ -41,10 +41,9 @@ class Label extends Base {
* Get all chats that have been assigned this Label
* @returns {Promise<Array<Chat>>}
*/
async getChats(){
async getChats() {
return this.client.getChatsByLabelId(this.id);
}
}
module.exports = Label;

View file

@ -32,7 +32,6 @@ class List {
*/
this.title = title;
/**
* footer of message
* @type {string}
@ -44,7 +43,6 @@ class List {
* @type {Array<any>}
*/
this.sections = this._format(sections);
}
/**
@ -55,25 +53,35 @@ class List {
* Input: [{title:'sectionTitle',rows:[{id:'customId', title:'ListItem2', description: 'desc'},{title:'ListItem2'}]}}]
* Returns: [{'title':'sectionTitle','rows':[{'rowId':'customId','title':'ListItem1','description':'desc'},{'rowId':'oGSRoD','title':'ListItem2','description':''}]}]
*/
_format(sections){
if(!sections.length){throw '[LT02] List without sections';}
if(sections.length > 1 && sections.filter(s => typeof s.title == 'undefined').length > 1){throw '[LT05] You can\'t have more than one empty title.';}
return sections.map( (section) =>{
if(!section.rows.length){throw '[LT03] Section without rows';}
_format(sections) {
if (!sections.length) {
throw '[LT02] List without sections';
}
if (
sections.length > 1 &&
sections.filter((s) => typeof s.title == 'undefined').length > 1
) {
throw "[LT05] You can't have more than one empty title.";
}
return sections.map((section) => {
if (!section.rows.length) {
throw '[LT03] Section without rows';
}
return {
title: section.title ? section.title : undefined,
rows: section.rows.map( (row) => {
if(!row.title){throw '[LT04] Row without title';}
rows: section.rows.map((row) => {
if (!row.title) {
throw '[LT04] Row without title';
}
return {
rowId: row.id ? row.id : Util.generateHash(6),
title: row.title,
description: row.description ? row.description : ''
description: row.description ? row.description : '',
};
})
}),
};
});
}
}
module.exports = List;

View file

@ -53,9 +53,10 @@ class Location {
* Location full description
* @type {string|undefined}
*/
this.description = this.name && this.address
? `${this.name}\n${this.address}`
: this.name || this.address || '';
this.description =
this.name && this.address
? `${this.name}\n${this.address}`
: this.name || this.address || '';
}
}

View file

@ -52,7 +52,9 @@ class Message extends Base {
* Message content
* @type {string}
*/
this.body = this.hasMedia ? data.caption || '' : data.body || data.pollName || data.eventName || '';
this.body = this.hasMedia
? data.caption || ''
: data.body || data.pollName || data.eventName || '';
/**
* Message type
@ -70,7 +72,10 @@ class Message extends Base {
* ID for the Chat that this message was sent to, except if the message was sent by the current user.
* @type {string}
*/
this.from = (typeof (data.from) === 'object' && data.from !== null) ? data.from._serialized : data.from;
this.from =
typeof data.from === 'object' && data.from !== null
? data.from._serialized
: data.from;
/**
* ID for who this message is for.
@ -79,19 +84,31 @@ class Message extends Base {
* If the message is sent by another user, it will be the ID for the current user.
* @type {string}
*/
this.to = (typeof (data.to) === 'object' && data.to !== null) ? data.to._serialized : data.to;
this.to =
typeof data.to === 'object' && data.to !== null
? data.to._serialized
: data.to;
/**
* If the message was sent to a group, this field will contain the user that sent the message.
* @type {string}
*/
this.author = (typeof (data.author) === 'object' && data.author !== null) ? data.author._serialized : data.author;
this.author =
typeof data.author === 'object' && data.author !== null
? data.author._serialized
: data.author;
/**
* String that represents from which device type the message was sent
* @type {string}
*/
this.deviceType = typeof data.id.id === 'string' && data.id.id.length > 21 ? 'android' : typeof data.id.id === 'string' && data.id.id.substring(0, 2) === '3A' ? 'ios' : 'web';
this.deviceType =
typeof data.id.id === 'string' && data.id.id.length > 21
? 'android'
: typeof data.id.id === 'string' &&
data.id.id.substring(0, 2) === '3A'
? 'ios'
: 'web';
/**
* Indicates if the message was forwarded
* @type {boolean}
@ -110,7 +127,8 @@ class Message extends Base {
* Indicates if the message is a status update
* @type {boolean}
*/
this.isStatus = data.isStatusV3 || data.id.remote === 'status@broadcast';
this.isStatus =
data.isStatusV3 || data.id.remote === 'status@broadcast';
/**
* Indicates if the message was starred
@ -162,7 +180,7 @@ class Message extends Base {
description = {
name: splitted[0],
address: splitted[1],
url: data.clientUrl
url: data.clientUrl,
};
}
return new Location(data.lat, data.lng, description);
@ -172,20 +190,36 @@ class Message extends Base {
* List of vCards contained in the message.
* @type {Array<string>}
*/
this.vCards = data.type === MessageTypes.CONTACT_CARD_MULTI ? data.vcardList.map((c) => c.vcard) : data.type === MessageTypes.CONTACT_CARD ? [data.body] : [];
this.vCards =
data.type === MessageTypes.CONTACT_CARD_MULTI
? data.vcardList.map((c) => c.vcard)
: data.type === MessageTypes.CONTACT_CARD
? [data.body]
: [];
/**
* Group Invite Data
* @type {object}
*/
this.inviteV4 = data.type === MessageTypes.GROUP_INVITE ? {
inviteCode: data.inviteCode,
inviteCodeExp: data.inviteCodeExp,
groupId: data.inviteGrp,
groupName: data.inviteGrpName,
fromId: typeof data.from === 'object' && '_serialized' in data.from ? data.from._serialized : data.from,
toId: typeof data.to === 'object' && '_serialized' in data.to ? data.to._serialized : data.to
} : undefined;
this.inviteV4 =
data.type === MessageTypes.GROUP_INVITE
? {
inviteCode: data.inviteCode,
inviteCodeExp: data.inviteCodeExp,
groupId: data.inviteGrp,
groupName: data.inviteGrpName,
fromId:
typeof data.from === 'object' &&
'_serialized' in data.from
? data.from._serialized
: data.from,
toId:
typeof data.to === 'object' &&
'_serialized' in data.to
? data.to._serialized
: data.to,
}
: undefined;
/**
* Indicates the mentions in the message body.
@ -284,17 +318,27 @@ class Message extends Base {
}
/** Selected List row Id **/
if (data.listResponse && data.listResponse.singleSelectReply.selectedRowId) {
this.selectedRowId = data.listResponse.singleSelectReply.selectedRowId;
if (
data.listResponse &&
data.listResponse.singleSelectReply.selectedRowId
) {
this.selectedRowId =
data.listResponse.singleSelectReply.selectedRowId;
}
if (this.type === MessageTypes.POLL_CREATION) {
this.pollName = data.pollName;
this.pollOptions = data.pollOptions;
this.allowMultipleAnswers = Boolean(!data.pollSelectableOptionsCount);
this.allowMultipleAnswers = Boolean(
!data.pollSelectableOptionsCount,
);
this.pollInvalidated = data.pollInvalidated;
this.isSentCagPollCreation = data.isSentCagPollCreation;
this.messageSecret = data.messageSecret ? Object.keys(data.messageSecret).map((key) => data.messageSecret[key]) : [];
this.messageSecret = data.messageSecret
? Object.keys(data.messageSecret).map(
(key) => data.messageSecret[key],
)
: [];
}
return super._patch(data);
@ -311,12 +355,18 @@ class Message extends Base {
*/
async reload() {
const newData = await this.client.pupPage.evaluate(async (msgId) => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
if (!msg) return null;
return window.WWebJS.getMessageModel(msg);
}, this.id._serialized);
if(!newData) return null;
if (!newData) return null;
this._patch(newData);
return this;
@ -351,7 +401,11 @@ class Message extends Base {
* @returns {Promise<Array<Contact>>}
*/
async getMentions() {
return await Promise.all(this.mentionedIds.map(async m => await this.client.getContactById(m)));
return await Promise.all(
this.mentionedIds.map(
async (m) => await this.client.getContactById(m),
),
);
}
/**
@ -359,7 +413,12 @@ class Message extends Base {
* @returns {Promise<Array<GroupChat>>}
*/
async getGroupMentions() {
return await Promise.all(this.groupMentions.map(async (m) => await this.client.getChatById(m.groupJid._serialized)));
return await Promise.all(
this.groupMentions.map(
async (m) =>
await this.client.getChatById(m.groupJid._serialized),
),
);
}
/**
@ -370,8 +429,16 @@ class Message extends Base {
if (!this.hasQuotedMsg) return undefined;
const quotedMsg = await this.client.pupPage.evaluate(async (msgId) => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
const quotedMsg = window.require('WAWebQuotedMsgModelUtils').getQuotedMsgObj(msg);
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
const quotedMsg = window
.require('WAWebQuotedMsgModelUtils')
.getQuotedMsgObj(msg);
return window.WWebJS.getMessageModel(quotedMsg);
}, this.id._serialized);
@ -395,7 +462,7 @@ class Message extends Base {
options = {
...options,
quotedMessageId: this.id._serialized
quotedMessageId: this.id._serialized,
};
return this.client.sendMessage(chatId, content, options);
@ -406,14 +473,26 @@ class Message extends Base {
* @param {string} reaction - Emoji to react with. Send an empty string to remove the reaction.
* @return {Promise}
*/
async react(reaction){
await this.client.pupPage.evaluate(async (messageId, reaction) => {
if (!messageId) return null;
const msg =
(window.require('WAWebCollections')).Msg.get(messageId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([messageId]))?.messages?.[0];
if(!msg) return null;
await (window.require('WAWebSendReactionMsgAction'))(msg, reaction);
}, this.id._serialized, reaction);
async react(reaction) {
await this.client.pupPage.evaluate(
async (messageId, reaction) => {
if (!messageId) return null;
const msg =
window.require('WAWebCollections').Msg.get(messageId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([messageId])
)?.messages?.[0];
if (!msg) return null;
await window.require('WAWebSendReactionMsgAction')(
msg,
reaction,
);
},
this.id._serialized,
reaction,
);
}
/**
@ -433,9 +512,13 @@ class Message extends Base {
async forward(chat) {
const chatId = typeof chat === 'string' ? chat : chat.id._serialized;
await this.client.pupPage.evaluate(async (msgId, chatId) => {
return window.WWebJS.forwardMessage(chatId, msgId);
}, this.id._serialized, chatId);
await this.client.pupPage.evaluate(
async (msgId, chatId) => {
return window.WWebJS.forwardMessage(chatId, msgId);
},
this.id._serialized,
chatId,
);
}
/**
@ -448,57 +531,84 @@ class Message extends Base {
}
const result = await this.client.pupPage.evaluate(async (msgId) => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
// REUPLOADING mediaStage means the media is expired and the download button is spinning, cannot be downloaded now
if (!msg || !msg.mediaData || msg.mediaData.mediaStage === 'REUPLOADING') {
if (
!msg ||
!msg.mediaData ||
msg.mediaData.mediaStage === 'REUPLOADING'
) {
return null;
}
if (msg.mediaData.mediaStage != 'RESOLVED') {
// try to resolve media
await msg.downloadMedia({
downloadEvenIfExpensive: true,
rmrReason: 1
rmrReason: 1,
});
}
if (msg.mediaData.mediaStage.includes('ERROR') || msg.mediaData.mediaStage === 'FETCHING') {
if (
msg.mediaData.mediaStage.includes('ERROR') ||
msg.mediaData.mediaStage === 'FETCHING'
) {
// media could not be downloaded
return undefined;
}
try {
const mockQpl = {
addAnnotations: function() { return this; },
addPoint: function() { return this; }
addAnnotations: function () {
return this;
},
addPoint: function () {
return this;
},
};
const decryptedMedia = await (window.require('WAWebDownloadManager').downloadManager).downloadAndMaybeDecrypt({
directPath: msg.directPath,
encFilehash: msg.encFilehash,
filehash: msg.filehash,
mediaKey: msg.mediaKey,
mediaKeyTimestamp: msg.mediaKeyTimestamp,
type: msg.type,
signal: (new AbortController).signal,
downloadQpl: mockQpl
});
const decryptedMedia = await window
.require('WAWebDownloadManager')
.downloadManager.downloadAndMaybeDecrypt({
directPath: msg.directPath,
encFilehash: msg.encFilehash,
filehash: msg.filehash,
mediaKey: msg.mediaKey,
mediaKeyTimestamp: msg.mediaKeyTimestamp,
type: msg.type,
signal: new AbortController().signal,
downloadQpl: mockQpl,
});
const data = await window.WWebJS.arrayBufferToBase64Async(decryptedMedia);
const data =
await window.WWebJS.arrayBufferToBase64Async(
decryptedMedia,
);
return {
data,
mimetype: msg.mimetype,
filename: msg.filename,
filesize: msg.size
filesize: msg.size,
};
} catch (e) {
if(e.status && e.status === 404) return undefined;
if (e.status && e.status === 404) return undefined;
throw e;
}
}, this.id._serialized);
if (!result) return undefined;
return new MessageMedia(result.mimetype, result.data, result.filename, result.filesize);
return new MessageMedia(
result.mimetype,
result.data,
result.filename,
result.filesize,
);
}
/**
@ -507,25 +617,66 @@ class Message extends Base {
* @param {?boolean} [clearMedia = true] If true, any associated media will also be deleted from a device.
*/
async delete(everyone, clearMedia = true) {
await this.client.pupPage.evaluate(async (msgId, everyone, clearMedia) => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
const chat = (window.require('WAWebCollections')).Chat.get(msg.id.remote) || (await (window.require('WAWebCollections')).Chat.find(msg.id.remote));
await this.client.pupPage.evaluate(
async (msgId, everyone, clearMedia) => {
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
const chat =
window
.require('WAWebCollections')
.Chat.get(msg.id.remote) ||
(await window
.require('WAWebCollections')
.Chat.find(msg.id.remote));
const canRevoke =
window.require('WAWebMsgActionCapability').canSenderRevokeMsg(msg) || window.require('WAWebMsgActionCapability').canAdminRevokeMsg(msg);
const canRevoke =
window
.require('WAWebMsgActionCapability')
.canSenderRevokeMsg(msg) ||
window
.require('WAWebMsgActionCapability')
.canAdminRevokeMsg(msg);
const { Cmd } = window.require('WAWebCmd');
const { Cmd } = window.require('WAWebCmd');
if (everyone && canRevoke) {
return window.WWebJS.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.0')
? Cmd.sendRevokeMsgs(chat, { list: [msg], type: 'message' }, { clearMedia: clearMedia })
: Cmd.sendRevokeMsgs(chat, [msg], { clearMedia: true, type: msg.id.fromMe ? 'Sender' : 'Admin' });
}
if (everyone && canRevoke) {
return window.WWebJS.compareWwebVersions(
window.Debug.VERSION,
'>=',
'2.3000.0',
)
? Cmd.sendRevokeMsgs(
chat,
{ list: [msg], type: 'message' },
{ clearMedia: clearMedia },
)
: Cmd.sendRevokeMsgs(chat, [msg], {
clearMedia: true,
type: msg.id.fromMe ? 'Sender' : 'Admin',
});
}
return window.WWebJS.compareWwebVersions(window.Debug.VERSION, '>=', '2.3000.0')
? Cmd.sendDeleteMsgs(chat, { list: [msg], type: 'message' }, clearMedia)
: Cmd.sendDeleteMsgs(chat, [msg], clearMedia);
}, this.id._serialized, everyone, clearMedia);
return window.WWebJS.compareWwebVersions(
window.Debug.VERSION,
'>=',
'2.3000.0',
)
? Cmd.sendDeleteMsgs(
chat,
{ list: [msg], type: 'message' },
clearMedia,
)
: Cmd.sendDeleteMsgs(chat, [msg], clearMedia);
},
this.id._serialized,
everyone,
clearMedia,
);
}
/**
@ -533,10 +684,20 @@ class Message extends Base {
*/
async star() {
await this.client.pupPage.evaluate(async (msgId) => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
if (window.require('WAWebMsgActionCapability').canStarMsg(msg)) {
let chat = await (window.require('WAWebCollections')).Chat.find(msg.id.remote);
return (window.require('WAWebCmd').Cmd).sendStarMsgs(chat, [msg], false);
let chat = await window
.require('WAWebCollections')
.Chat.find(msg.id.remote);
return window
.require('WAWebCmd')
.Cmd.sendStarMsgs(chat, [msg], false);
}
}, this.id._serialized);
}
@ -546,10 +707,20 @@ class Message extends Base {
*/
async unstar() {
await this.client.pupPage.evaluate(async (msgId) => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
if (window.require('WAWebMsgActionCapability').canStarMsg(msg)) {
let chat = await (window.require('WAWebCollections')).Chat.find(msg.id.remote);
return (window.require('WAWebCmd').Cmd).sendUnstarMsgs(chat, [msg], false);
let chat = await window
.require('WAWebCollections')
.Chat.find(msg.id.remote);
return window
.require('WAWebCmd')
.Cmd.sendUnstarMsgs(chat, [msg], false);
}
}, this.id._serialized);
}
@ -560,9 +731,17 @@ class Message extends Base {
* @returns {Promise<boolean>} Returns true if the operation completed successfully, false otherwise
*/
async pin(duration) {
return await this.client.pupPage.evaluate(async (msgId, duration) => {
return await window.WWebJS.pinUnpinMsgAction(msgId, 1, duration);
}, this.id._serialized, duration);
return await this.client.pupPage.evaluate(
async (msgId, duration) => {
return await window.WWebJS.pinUnpinMsgAction(
msgId,
1,
duration,
);
},
this.id._serialized,
duration,
);
}
/**
@ -593,13 +772,28 @@ class Message extends Base {
*/
async getInfo() {
const info = await this.client.pupPage.evaluate(async (msgId) => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
if (!msg || !msg.id.fromMe) return null;
return new Promise((resolve) => {
setTimeout(async () => {
resolve(await (window.require('WAWebApiMessageInfoStore').queryMsgInfo)(msg.id));
}, (Date.now() - msg.t * 1000 < 1250) && Math.floor(Math.random() * (1200 - 1100 + 1)) + 1100 || 0);
setTimeout(
async () => {
resolve(
await window
.require('WAWebApiMessageInfoStore')
.queryMsgInfo(msg.id),
);
},
(Date.now() - msg.t * 1000 < 1250 &&
Math.floor(Math.random() * (1200 - 1100 + 1)) + 1100) ||
0,
);
});
}, this.id._serialized);
@ -612,9 +806,14 @@ class Message extends Base {
*/
async getOrder() {
if (this.type === MessageTypes.ORDER) {
const result = await this.client.pupPage.evaluate((orderId, token, chatId) => {
return window.WWebJS.getOrderDetail(orderId, token, chatId);
}, this.orderId, this.token, this._getChatId());
const result = await this.client.pupPage.evaluate(
(orderId, token, chatId) => {
return window.WWebJS.getOrderDetail(orderId, token, chatId);
},
this.orderId,
this.token,
this._getChatId(),
);
if (!result) return undefined;
return new Order(this.client, result);
}
@ -627,8 +826,14 @@ class Message extends Base {
async getPayment() {
if (this.type === MessageTypes.PAYMENT) {
const msg = await this.client.pupPage.evaluate(async (msgId) => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
if(!msg) return null;
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
if (!msg) return null;
return msg.serialize();
}, this.id._serialized);
return new Payment(this.client, msg);
@ -636,7 +841,6 @@ class Message extends Base {
return undefined;
}
/**
* Reaction List
* @typedef {Object} ReactionList
@ -656,7 +860,9 @@ class Message extends Base {
}
const reactions = await this.client.pupPage.evaluate(async (msgId) => {
const msgReactions = await (window.require('WAWebCollections')).Reactions.find(msgId);
const msgReactions = await window
.require('WAWebCollections')
.Reactions.find(msgId);
if (!msgReactions || !msgReactions.reactions.length) return null;
return msgReactions.reactions.serialize();
}, this.id._serialized);
@ -665,8 +871,8 @@ class Message extends Base {
return undefined;
}
return reactions.map(reaction => {
reaction.senders = reaction.senders.map(sender => {
return reactions.map((reaction) => {
reaction.senders = reaction.senders.map((sender) => {
sender.timestamp = Math.round(sender.timestamp / 1000);
return new Reaction(this.client, sender);
});
@ -682,36 +888,68 @@ class Message extends Base {
*/
async edit(content, options = {}) {
if (options.mentions) {
!Array.isArray(options.mentions) && (options.mentions = [options.mentions]);
if (options.mentions.some((possiblyContact) => possiblyContact instanceof Contact)) {
console.warn('Mentions with an array of Contact are now deprecated. See more at https://github.com/pedroslopez/whatsapp-web.js/pull/2166.');
options.mentions = options.mentions.map((a) => a.id._serialized);
!Array.isArray(options.mentions) &&
(options.mentions = [options.mentions]);
if (
options.mentions.some(
(possiblyContact) => possiblyContact instanceof Contact,
)
) {
console.warn(
'Mentions with an array of Contact are now deprecated. See more at https://github.com/pedroslopez/whatsapp-web.js/pull/2166.',
);
options.mentions = options.mentions.map(
(a) => a.id._serialized,
);
}
}
options.groupMentions && !Array.isArray(options.groupMentions) && (options.groupMentions = [options.groupMentions]);
options.groupMentions &&
!Array.isArray(options.groupMentions) &&
(options.groupMentions = [options.groupMentions]);
let internalOptions = {
linkPreview: options.linkPreview === false ? undefined : true,
mentionedJidList: options.mentions || [],
groupMentions: options.groupMentions,
extraOptions: options.extra
extraOptions: options.extra,
};
if (!this.fromMe) {
return null;
}
const messageEdit = await this.client.pupPage.evaluate(async (msgId, message, options) => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
if (!msg) return null;
const messageEdit = await this.client.pupPage.evaluate(
async (msgId, message, options) => {
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
if (!msg) return null;
let canEdit = window.require('WAWebMsgActionCapability').canEditText(msg) || window.require('WAWebMsgActionCapability').canEditCaption(msg);
if (canEdit) {
const msgEdit = await window.WWebJS.editMessage(msg, message, options);
return msgEdit.serialize();
}
return null;
}, this.id._serialized, content, internalOptions);
let canEdit =
window
.require('WAWebMsgActionCapability')
.canEditText(msg) ||
window
.require('WAWebMsgActionCapability')
.canEditCaption(msg);
if (canEdit) {
const msgEdit = await window.WWebJS.editMessage(
msg,
message,
options,
);
return msgEdit.serialize();
}
return null;
},
this.id._serialized,
content,
internalOptions,
);
if (messageEdit) {
return new Message(this.client, messageEdit);
}
@ -729,25 +967,40 @@ class Message extends Base {
return null;
}
const edittedEventMsg = await this.client.pupPage.evaluate(async (msgId, editedEventObject) => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
if (!msg) return null;
const edittedEventMsg = await this.client.pupPage.evaluate(
async (msgId, editedEventObject) => {
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
if (!msg) return null;
const { name, startTimeTs, eventSendOptions } = editedEventObject;
const eventOptions = {
name: name,
description: eventSendOptions.description,
startTime: startTimeTs,
endTime: eventSendOptions.endTimeTs,
location: eventSendOptions.location,
callType: eventSendOptions.callType,
isEventCanceled: eventSendOptions.isEventCanceled,
};
const { name, startTimeTs, eventSendOptions } =
editedEventObject;
const eventOptions = {
name: name,
description: eventSendOptions.description,
startTime: startTimeTs,
endTime: eventSendOptions.endTimeTs,
location: eventSendOptions.location,
callType: eventSendOptions.callType,
isEventCanceled: eventSendOptions.isEventCanceled,
};
await (window.require('WAWebSendEventEditMsgAction')).sendEventEditMessage(eventOptions, msg);
const editedMsg = (window.require('WAWebCollections')).Msg.get(msg.id._serialized);
return editedMsg?.serialize();
}, this.id._serialized, editedEventObject);
await window
.require('WAWebSendEventEditMsgAction')
.sendEventEditMessage(eventOptions, msg);
const editedMsg = window
.require('WAWebCollections')
.Msg.get(msg.id._serialized);
return editedMsg?.serialize();
},
this.id._serialized,
editedEventObject,
);
return edittedEventMsg && new Message(this.client, edittedEventMsg);
}
@ -765,24 +1018,36 @@ class Message extends Base {
* @returns {Promise}
*/
async vote(selectedOptions) {
if (this.type != MessageTypes.POLL_CREATION) throw 'Invalid usage! Can only be used with a pollCreation message';
if (this.type != MessageTypes.POLL_CREATION)
throw 'Invalid usage! Can only be used with a pollCreation message';
await this.client.pupPage.evaluate(async (messageId, votes) => {
if (!messageId) return null;
if (!Array.isArray(votes)) votes = [votes];
let localIdSet = new Set();
const msg =
(window.require('WAWebCollections')).Msg.get(messageId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([messageId]))?.messages?.[0];
if (!msg) return null;
await this.client.pupPage.evaluate(
async (messageId, votes) => {
if (!messageId) return null;
if (!Array.isArray(votes)) votes = [votes];
let localIdSet = new Set();
const msg =
window.require('WAWebCollections').Msg.get(messageId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([messageId])
)?.messages?.[0];
if (!msg) return null;
msg.pollOptions.forEach(a => {
for (const option of votes) {
if (a.name === option) localIdSet.add(a.localId);
}
});
msg.pollOptions.forEach((a) => {
for (const option of votes) {
if (a.name === option) localIdSet.add(a.localId);
}
});
await (window.require('WAWebPollsSendVoteMsgAction')).sendVote(msg, localIdSet);
}, this.id._serialized, selectedOptions);
await window
.require('WAWebPollsSendVoteMsgAction')
.sendVote(msg, localIdSet);
},
this.id._serialized,
selectedOptions,
);
}
}

View file

@ -46,7 +46,7 @@ class MessageMedia {
* @returns {MessageMedia}
*/
static fromFilePath(filePath) {
const b64data = fs.readFileSync(filePath, {encoding: 'base64'});
const b64data = fs.readFileSync(filePath, { encoding: 'base64' });
const mimetype = mime.getType(filePath);
const filename = path.basename(filePath);
@ -69,16 +69,25 @@ class MessageMedia {
let mimetype = mime.getType(pUrl.pathname);
if (!mimetype && !options.unsafeMime)
throw new Error('Unable to determine MIME type using URL. Set unsafeMime to true to download it anyway.');
throw new Error(
'Unable to determine MIME type using URL. Set unsafeMime to true to download it anyway.',
);
async function fetchData (url, options) {
const reqOptions = Object.assign({ headers: { accept: 'image/* video/* text/* audio/*' } }, options);
async function fetchData(url, options) {
const reqOptions = Object.assign(
{ headers: { accept: 'image/* video/* text/* audio/*' } },
options,
);
const response = await fetch(url, reqOptions);
const mime = response.headers.get('Content-Type');
const size = response.headers.get('Content-Length');
const contentDisposition = response.headers.get('Content-Disposition');
const name = contentDisposition ? contentDisposition.match(/((?<=filename=")(.*)(?="))/) : null;
const contentDisposition = response.headers.get(
'Content-Disposition',
);
const name = contentDisposition
? contentDisposition.match(/((?<=filename=")(.*)(?="))/)
: null;
let data = '';
if (response.buffer) {
@ -95,14 +104,18 @@ class MessageMedia {
}
const res = options.client
? (await options.client.pupPage.evaluate(fetchData, url, options.reqOptions))
: (await fetchData(url, options.reqOptions));
? await options.client.pupPage.evaluate(
fetchData,
url,
options.reqOptions,
)
: await fetchData(url, options.reqOptions);
const filename = options.filename ||
(res.name ? res.name[0] : (pUrl.pathname.split('/').pop() || 'file'));
const filename =
options.filename ||
(res.name ? res.name[0] : pUrl.pathname.split('/').pop() || 'file');
if (!mimetype)
mimetype = res.mime;
if (!mimetype) mimetype = res.mime;
return new MessageMedia(mimetype, res.data, filename, res.size || null);
}

View file

@ -20,7 +20,9 @@ class Order extends Base {
* @type {Array<Product>}
*/
if (data.products) {
this.products = data.products.map(product => new Product(this.client, product));
this.products = data.products.map(
(product) => new Product(this.client, product),
);
}
/**
* Order Subtotal
@ -45,8 +47,6 @@ class Order extends Base {
return super._patch(data);
}
}
module.exports = Order;

View file

@ -69,11 +69,14 @@ class Payment extends Base {
* The note sent with the payment
* @type {string}
*/
this.paymentNote = !data.paymentNoteMsg ? undefined : data.paymentNoteMsg.body ? data.paymentNoteMsg.body : undefined ;
this.paymentNote = !data.paymentNoteMsg
? undefined
: data.paymentNoteMsg.body
? data.paymentNoteMsg.body
: undefined;
return super._patch(data);
}
}
module.exports = Payment;

View file

@ -27,7 +27,7 @@ class Poll {
*/
this.pollOptions = pollOptions.map((option, index) => ({
name: option.trim(),
localId: index
localId: index,
}));
/**
@ -36,7 +36,7 @@ class Poll {
*/
this.options = {
allowMultipleAnswers: options.allowMultipleAnswers === true,
messageSecret: options.messageSecret
messageSecret: options.messageSecret,
};
}
}

View file

@ -35,15 +35,18 @@ class PollVote extends Base {
* @type {SelectedPollOption[]}
*/
if (data.selectedOptionLocalIds.length > 0) {
if(data.parentMessage) { // temporary failsafe
if (data.parentMessage) {
// temporary failsafe
this.selectedOptions = data.selectedOptionLocalIds.map((e) => ({
name: data.parentMessage.pollOptions.find((x) => x.localId === e).name,
localId: e
name: data.parentMessage.pollOptions.find(
(x) => x.localId === e,
).name,
localId: e,
}));
} else {
this.selectedOptions = data.selectedOptionLocalIds.map((e) => ({
name: undefined,
localId: e
localId: e,
}));
}
} else {
@ -66,7 +69,7 @@ class PollVote extends Base {
* The poll creation message id
* @type {Object}
*/
this.parentMsgKey = data.parentMsgKey;
this.parentMsgKey = data.parentMsgKey;
return super._patch(data);
}

View file

@ -6,8 +6,6 @@ const Chat = require('./Chat');
* Represents a Private Chat on WhatsApp
* @extends {Chat}
*/
class PrivateChat extends Chat {
}
class PrivateChat extends Chat {}
module.exports = PrivateChat;

View file

@ -6,8 +6,6 @@ const Contact = require('./Contact');
* Represents a Private Contact on WhatsApp
* @extends {Contact}
*/
class PrivateContact extends Contact {
}
class PrivateContact extends Contact {}
module.exports = PrivateContact;

View file

@ -19,7 +19,6 @@ class ProductMetadata extends Base {
return super._patch(data);
}
}
module.exports = ProductMetadata;

View file

@ -60,10 +60,8 @@ class Reaction extends Base {
*/
this.ack = data.ack;
return super._patch(data);
}
}
module.exports = Reaction;

View file

@ -37,11 +37,13 @@ class ScheduledEvent {
*/
this.eventSendOptions = {
description: options.description?.trim(),
endTimeTs: options.endTime ? Math.floor(options.endTime.getTime() / 1000) : null,
endTimeTs: options.endTime
? Math.floor(options.endTime.getTime() / 1000)
: null,
location: options.location?.trim(),
callType: this._validateInputs('callType', options.callType),
isEventCanceled: options.isEventCanceled ?? false,
messageSecret: options.messageSecret
messageSecret: options.messageSecret,
};
}
@ -53,15 +55,25 @@ class ScheduledEvent {
*/
_validateInputs(propName, propValue) {
if (propName === 'name' && !propValue) {
throw new class CreateScheduledEventError extends Error {
constructor(m) { super(m); }
}(`Empty '${propName}' parameter value is provided.`);
throw new (class CreateScheduledEventError extends Error {
constructor(m) {
super(m);
}
})(`Empty '${propName}' parameter value is provided.`);
}
if (propName === 'callType' && propValue && !['video', 'voice', 'none'].includes(propValue)) {
throw new class CreateScheduledEventError extends Error {
constructor(m) { super(m); }
}(`Invalid '${propName}' parameter value is provided. Valid values are: 'voice' | 'video' | 'none'.`);
if (
propName === 'callType' &&
propValue &&
!['video', 'voice', 'none'].includes(propValue)
) {
throw new (class CreateScheduledEventError extends Error {
constructor(m) {
super(m);
}
})(
`Invalid '${propName}' parameter value is provided. Valid values are: 'voice' | 'video' | 'none'.`,
);
}
return propValue;

View file

@ -5,7 +5,7 @@ exports.WhatsWebURL = 'https://web.whatsapp.com/';
exports.DefaultOptions = {
puppeteer: {
headless: true,
defaultViewport: null
defaultViewport: null,
},
webVersion: '2.3000.1017054665',
webVersionCache: {
@ -15,7 +15,8 @@ exports.DefaultOptions = {
qrMaxRetries: 0,
takeoverOnConflict: false,
takeoverTimeoutMs: 0,
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36',
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36',
ffmpegPath: 'ffmpeg',
bypassCSP: false,
proxyAuthentication: undefined,
@ -34,7 +35,7 @@ exports.DefaultOptions = {
exports.Status = {
INITIALIZING: 0,
AUTHENTICATING: 1,
READY: 3
READY: 3,
};
/**
@ -72,7 +73,7 @@ exports.Events = {
BATTERY_CHANGED: 'change_battery',
INCOMING_CALL: 'call',
REMOTE_SESSION_SAVED: 'remote_session_saved',
VOTE_UPDATE: 'vote_update'
VOTE_UPDATE: 'vote_update',
};
/**
@ -148,7 +149,7 @@ exports.GroupNotificationTypes = {
exports.ChatTypes = {
SOLO: 'solo',
GROUP: 'group',
UNKNOWN: 'unknown'
UNKNOWN: 'unknown',
};
/**
@ -168,7 +169,7 @@ exports.WAState = {
TOS_BLOCK: 'TOS_BLOCK',
UNLAUNCHED: 'UNLAUNCHED',
UNPAIRED: 'UNPAIRED',
UNPAIRED_IDLE: 'UNPAIRED_IDLE'
UNPAIRED_IDLE: 'UNPAIRED_IDLE',
};
/**

View file

@ -5,8 +5,12 @@ exports.ExposeAuthStore = () => {
window.AuthStore.AppState = window.require('WAWebSocketModel').Socket;
window.AuthStore.Cmd = window.require('WAWebCmd').Cmd;
window.AuthStore.Conn = window.require('WAWebConnModel').Conn;
window.AuthStore.OfflineMessageHandler = window.require('WAWebOfflineHandler').OfflineMessageHandler;
window.AuthStore.PairingCodeLinkUtils = window.require('WAWebAltDeviceLinkingApi');
window.AuthStore.OfflineMessageHandler = window.require(
'WAWebOfflineHandler',
).OfflineMessageHandler;
window.AuthStore.PairingCodeLinkUtils = window.require(
'WAWebAltDeviceLinkingApi',
);
window.AuthStore.Base64Tools = window.require('WABase64');
window.AuthStore.RegistrationUtils = {
...window.require('WAWebCompanionRegClientUtils'),

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,6 @@
* Interface Controller
*/
class InterfaceController {
constructor(props) {
this.pupPage = props.pupPage;
}
@ -15,8 +14,12 @@ class InterfaceController {
*/
async openChatWindow(chatId) {
return await this.pupPage.evaluate(async (chatId) => {
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
return await (window.require('WAWebCmd').Cmd).openChatBottom({'chat':chat});
const chat = await window.WWebJS.getChat(chatId, {
getAsModel: false,
});
return await window
.require('WAWebCmd')
.Cmd.openChatBottom({ chat: chat });
}, chatId);
}
@ -25,9 +28,11 @@ class InterfaceController {
* @param {string} chatId ID of the chat drawer that will be opened
*/
async openChatDrawer(chatId) {
await this.pupPage.evaluate(async chatId => {
let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
await (window.require('WAWebCmd').Cmd).openDrawerMid(chat);
await this.pupPage.evaluate(async (chatId) => {
let chat = await window.WWebJS.getChat(chatId, {
getAsModel: false,
});
await window.require('WAWebCmd').Cmd.openDrawerMid(chat);
}, chatId);
}
@ -36,9 +41,11 @@ class InterfaceController {
* @param {string} chatId ID of the chat search that will be opened
*/
async openChatSearch(chatId) {
await this.pupPage.evaluate(async chatId => {
let chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
await (window.require('WAWebCmd').Cmd).chatSearch(chat);
await this.pupPage.evaluate(async (chatId) => {
let chat = await window.WWebJS.getChat(chatId, {
getAsModel: false,
});
await window.require('WAWebCmd').Cmd.chatSearch(chat);
}, chatId);
}
@ -48,10 +55,24 @@ class InterfaceController {
*/
async openChatWindowAt(msgId) {
await this.pupPage.evaluate(async (msgId) => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
const chat = (window.require('WAWebCollections')).Chat.get(msg.id.remote) ?? await (window.require('WAWebCollections')).Chat.find(msg.id.remote);
const searchContext = await (window.require('WAWebChatMessageSearch')).getSearchContext(chat, msg.id);
await (window.require('WAWebCmd').Cmd).openChatAt({ chat: chat, msgContext: searchContext });
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
const chat =
window.require('WAWebCollections').Chat.get(msg.id.remote) ??
(await window
.require('WAWebCollections')
.Chat.find(msg.id.remote));
const searchContext = await window
.require('WAWebChatMessageSearch')
.getSearchContext(chat, msg.id);
await window
.require('WAWebCmd')
.Cmd.openChatAt({ chat: chat, msgContext: searchContext });
}, msgId);
}
@ -60,9 +81,15 @@ class InterfaceController {
* @param {string} msgId ID of the message drawer that will be opened
*/
async openMessageDrawer(msgId) {
await this.pupPage.evaluate(async msgId => {
const msg = (window.require('WAWebCollections')).Msg.get(msgId) || (await (window.require('WAWebCollections')).Msg.getMessagesById([msgId]))?.messages?.[0];
await (window.require('WAWebCmd').Cmd).msgInfoDrawer(msg);
await this.pupPage.evaluate(async (msgId) => {
const msg =
window.require('WAWebCollections').Msg.get(msgId) ||
(
await window
.require('WAWebCollections')
.Msg.getMessagesById([msgId])
)?.messages?.[0];
await window.require('WAWebCmd').Cmd.msgInfoDrawer(msg);
}, msgId);
}
@ -71,7 +98,9 @@ class InterfaceController {
*/
async closeRightDrawer() {
await this.pupPage.evaluate(async () => {
await window.require('WAWebDrawerManager').DrawerManager.closeDrawerRight();
await window
.require('WAWebDrawerManager')
.DrawerManager.closeDrawerRight();
});
}
@ -80,8 +109,11 @@ class InterfaceController {
*/
async getFeatures() {
return await this.pupPage.evaluate(() => {
if(!(window.require('WAWebCollections')).Features) throw new Error('This version of Whatsapp Web does not support features');
return (window.require('WAWebCollections')).Features.F;
if (!window.require('WAWebCollections').Features)
throw new Error(
'This version of Whatsapp Web does not support features',
);
return window.require('WAWebCollections').Features.F;
});
}
@ -91,8 +123,13 @@ class InterfaceController {
*/
async checkFeatureStatus(feature) {
return await this.pupPage.evaluate((feature) => {
if(!(window.require('WAWebCollections')).Features) throw new Error('This version of Whatsapp Web does not support features');
return (window.require('WAWebCollections')).Features.supportsFeature(feature);
if (!window.require('WAWebCollections').Features)
throw new Error(
'This version of Whatsapp Web does not support features',
);
return window
.require('WAWebCollections')
.Features.supportsFeature(feature);
}, feature);
}
@ -102,9 +139,14 @@ class InterfaceController {
*/
async enableFeatures(features) {
await this.pupPage.evaluate((features) => {
if(!(window.require('WAWebCollections')).Features) throw new Error('This version of Whatsapp Web does not support features');
if (!window.require('WAWebCollections').Features)
throw new Error(
'This version of Whatsapp Web does not support features',
);
for (const feature in features) {
(window.require('WAWebCollections')).Features.setFeature(features[feature], true);
window
.require('WAWebCollections')
.Features.setFeature(features[feature], true);
}
}, features);
}
@ -115,9 +157,14 @@ class InterfaceController {
*/
async disableFeatures(features) {
await this.pupPage.evaluate((features) => {
if(!(window.require('WAWebCollections')).Features) throw new Error('This version of Whatsapp Web does not support features');
if (!window.require('WAWebCollections').Features)
throw new Error(
'This version of Whatsapp Web does not support features',
);
for (const feature in features) {
(window.require('WAWebCollections')).Features.setFeature(features[feature], false);
window
.require('WAWebCollections')
.Features.setFeature(features[feature], false);
}
}, features);
}

View file

@ -20,4 +20,4 @@ async function exposeFunctionIfAbsent(page, name, fn) {
await page.exposeFunction(name, fn);
}
module.exports = {exposeFunctionIfAbsent};
module.exports = { exposeFunctionIfAbsent };

View file

@ -13,15 +13,20 @@ const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
*/
class Util {
constructor() {
throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
throw new Error(
`The ${this.constructor.name} class may not be instantiated.`,
);
}
static generateHash(length) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
result += characters.charAt(
Math.floor(Math.random() * charactersLength),
);
}
return result;
}
@ -79,13 +84,13 @@ class Util {
const tempFile = path.join(
tmpdir(),
`${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp`
`${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp`,
);
const stream = new (require('stream').Readable)();
const buffer = Buffer.from(
media.data.replace(`data:${media.mimetype};base64,`, ''),
'base64'
'base64',
);
stream.push(buffer);
stream.push(null);
@ -100,7 +105,7 @@ class Util {
'libwebp',
'-vf',
// eslint-disable-next-line no-useless-escape
'scale=\'iw*min(300/iw\,300/ih)\':\'ih*min(300/iw\,300/ih)\',format=rgba,pad=300:300:\'(300-iw)/2\':\'(300-ih)/2\':\'#00000000\',setsar=1,fps=10',
"scale='iw*min(300/iw\,300/ih)':'ih*min(300/iw\,300/ih)',format=rgba,pad=300:300:'(300-iw)/2':'(300-ih)/2':'#00000000',setsar=1,fps=10",
'-loop',
'0',
'-ss',
@ -151,8 +156,7 @@ class Util {
webpMedia = await this.formatImageToWebpSticker(media, pupPage);
else if (media.mimetype.includes('video'))
webpMedia = await this.formatVideoToWebpSticker(media);
else
throw new Error('Invalid media format');
else throw new Error('Invalid media format');
if (metadata.name || metadata.author) {
const img = new webp.Image();
@ -161,8 +165,17 @@ class Util {
const packname = metadata.name;
const author = metadata.author;
const categories = metadata.categories || [''];
const json = { 'sticker-pack-id': stickerPackId, 'sticker-pack-name': packname, 'sticker-pack-publisher': author, 'emojis': categories };
let exifAttr = Buffer.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00]);
const json = {
'sticker-pack-id': stickerPackId,
'sticker-pack-name': packname,
'sticker-pack-publisher': author,
emojis: categories,
};
let exifAttr = Buffer.from([
0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00,
0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00,
0x00, 0x00,
]);
let jsonBuffer = Buffer.from(JSON.stringify(json), 'utf8');
let exif = Buffer.concat([exifAttr, jsonBuffer]);
exif.writeUIntLE(jsonBuffer.length, 14, 4);

View file

@ -22,9 +22,11 @@ class LocalWebCache extends WebCache {
try {
return fs.readFileSync(filePath, 'utf-8');
}
catch (err) {
if (this.strict) throw new VersionResolveError(`Couldn't load version ${version} from the cache`);
} catch (err) {
if (this.strict)
throw new VersionResolveError(
`Couldn't load version ${version} from the cache`,
);
return null;
}
}

View file

@ -11,7 +11,10 @@ class RemoteWebCache extends WebCache {
constructor(options = {}) {
super();
if (!options.remotePath) throw new Error('webVersionCache.remotePath is required when using the remote cache');
if (!options.remotePath)
throw new Error(
'webVersionCache.remotePath is required when using the remote cache',
);
this.remotePath = options.remotePath;
this.strict = options.strict || false;
}
@ -28,7 +31,10 @@ class RemoteWebCache extends WebCache {
console.error(`Error fetching version ${version} from remote`, err);
}
if (this.strict) throw new VersionResolveError(`Couldn't load version ${version} from the archive`);
if (this.strict)
throw new VersionResolveError(
`Couldn't load version ${version} from the archive`,
);
return null;
}

View file

@ -2,13 +2,15 @@
* Default implementation of a web version cache that does nothing.
*/
class WebCache {
async resolve() { return null; }
async persist() { }
async resolve() {
return null;
}
async persist() {}
}
class VersionResolveError extends Error { }
class VersionResolveError extends Error {}
module.exports = {
WebCache,
VersionResolveError
VersionResolveError,
};

View file

@ -4,14 +4,14 @@ const { WebCache } = require('./WebCache');
const createWebCache = (type, options) => {
switch (type) {
case 'remote':
return new RemoteWebCache(options);
case 'local':
return new LocalWebCache(options);
case 'none':
return new WebCache();
default:
throw new Error(`Invalid WebCache type ${type}`);
case 'remote':
return new RemoteWebCache(options);
case 'local':
return new LocalWebCache(options);
case 'none':
return new WebCache();
default:
throw new Error(`Invalid WebCache type ${type}`);
}
};

View file

@ -1,14 +1,16 @@
## Running tests
# Running Tests
These tests require an authenticated WhatsApp Web session, as well as an additional phone that you can send messages to.
This can be configured using the following environment variables:
- `WWEBJS_TEST_SESSION`: A JSON-formatted string with legacy auth session details. Must include `WABrowserId`, `WASecretBundle`, `WAToken1` and `WAToken2`.
- `WWEBJS_TEST_SESSION_PATH`: Path to a JSON file that contains the legacy auth session details. Must include `WABrowserId`, `WASecretBundle`, `WAToken1` and `WAToken2`.
- `WWEBJS_TEST_CLIENT_ID`: `clientId` to use for local file based authentication.
- `WWEBJS_TEST_REMOTE_ID`: A valid WhatsApp ID that you can send messages to, e.g. `123456789@c.us`. It should be different from the ID used by the provided session.
You *must* set `WWEBJS_TEST_REMOTE_ID` **and** either `WWEBJS_TEST_SESSION`, `WWEBJS_TEST_SESSION_PATH` or `WWEBJS_TEST_CLIENT_ID` for the tests to run properly.
- `WWEBJS_TEST_CLIENT_ID`: `clientId` to use for local file based authentication (required for authenticated tests).
- `WWEBJS_TEST_REMOTE_ID`: A valid WhatsApp ID that you can send messages to, e.g. `123456789@c.us`. It should be different from the ID used by the provided session (required).
### Multidevice
Some of the tested functionality depends on whether the account has multidevice enabled or not. If you are using multidevice, you should set `WWEBJS_TEST_MD=1`.
You can create a `.env` file in the root directory with these variables. See `.env.example` for a template.
To run the tests:
```bash
npm test
```

View file

@ -15,7 +15,7 @@ chai.use(chaiAsPromised);
const remoteId = helper.remoteId;
describe('Client', function() {
describe('Client', function () {
describe('User Agent', function () {
it('should set user agent on browser', async function () {
this.timeout(25000);
@ -28,7 +28,9 @@ describe('Client', function() {
const browserUA = await client.pupBrowser.userAgent();
expect(browserUA).to.equal(DefaultOptions.userAgent);
const pageUA = await client.pupPage.evaluate(() => window.navigator.userAgent);
const pageUA = await client.pupPage.evaluate(
() => window.navigator.userAgent,
);
expect(pageUA).to.equal(DefaultOptions.userAgent);
await client.destroy();
@ -36,12 +38,15 @@ describe('Client', function() {
it('should set custom user agent on browser', async function () {
this.timeout(25000);
const customUA = DefaultOptions.userAgent.replace(/Chrome\/.* /, 'Chrome/99.9.9999.999 ');
const customUA = DefaultOptions.userAgent.replace(
/Chrome\/.* /,
'Chrome/99.9.9999.999 ',
);
const client = helper.createClient({
options: {
userAgent: customUA
}
userAgent: customUA,
},
});
client.initialize();
@ -51,7 +56,9 @@ describe('Client', function() {
expect(browserUA).to.equal(customUA);
expect(browserUA.includes('Chrome/99.9.9999.999')).to.equal(true);
const pageUA = await client.pupPage.evaluate(() => window.navigator.userAgent);
const pageUA = await client.pupPage.evaluate(
() => window.navigator.userAgent,
);
expect(pageUA).to.equal(customUA);
await client.destroy();
@ -60,14 +67,17 @@ describe('Client', function() {
it('should respect an existing user agent arg', async function () {
this.timeout(25000);
const customUA = DefaultOptions.userAgent.replace(/Chrome\/.* /, 'Chrome/99.9.9999.999 ');
const customUA = DefaultOptions.userAgent.replace(
/Chrome\/.* /,
'Chrome/99.9.9999.999 ',
);
const client = helper.createClient({
options: {
puppeteer: {
args: [`--user-agent=${customUA}`]
}
}
args: [`--user-agent=${customUA}`],
},
},
});
client.initialize();
@ -77,7 +87,9 @@ describe('Client', function() {
expect(browserUA).to.equal(customUA);
expect(browserUA.includes('Chrome/99.9.9999.999')).to.equal(true);
const pageUA = await client.pupPage.evaluate(() => window.navigator.userAgent);
const pageUA = await client.pupPage.evaluate(
() => window.navigator.userAgent,
);
expect(pageUA).to.equal(DefaultOptions.userAgent);
await client.destroy();
@ -107,7 +119,9 @@ describe('Client', function() {
const qrCallback = sinon.spy();
const disconnectedCallback = sinon.spy();
const client = helper.createClient({ options: { qrMaxRetries: 2 } });
const client = helper.createClient({
options: { qrMaxRetries: 2 },
});
client.on('qr', qrCallback);
client.on('disconnected', disconnectedCallback);
@ -116,7 +130,11 @@ describe('Client', function() {
await helper.sleep(45000);
expect(qrCallback.calledThrice).to.eql(true);
expect(disconnectedCallback.calledOnceWith('Max qrcode retries reached')).to.eql(true);
expect(
disconnectedCallback.calledOnceWith(
'Max qrcode retries reached',
),
).to.eql(true);
});
it('should authenticate with existing session', async function () {
@ -145,12 +163,12 @@ describe('Client', function() {
});
});
describe('Authenticated', function() {
describe('Authenticated', function () {
let client;
before(async function() {
before(async function () {
this.timeout(35000);
client = helper.createClient({authenticated: true});
client = helper.createClient({ authenticated: true });
await client.initialize();
});
@ -164,9 +182,8 @@ describe('Client', function() {
console.log(`WA Version: ${version}`);
});
describe('Send Messages', function () {
it('can send a message', async function() {
it('can send a message', async function () {
const msg = await client.sendMessage(remoteId, 'hello world');
expect(msg).to.be.instanceOf(Message);
expect(msg.type).to.equal(MessageTypes.TEXT);
@ -175,23 +192,27 @@ describe('Client', function() {
expect(msg.to).to.equal(remoteId);
});
it('can send a media message', async function() {
it('can send a media message', async function () {
const media = new MessageMedia(
'image/png',
'iVBORw0KGgoAAAANSUhEUgAAAV4AAACWBAMAAABkyf1EAAAAG1BMVEXMzMyWlpacnJyqqqrFxcWxsbGjo6O3t7e+vr6He3KoAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAEcElEQVR4nO2aTW/bRhCGh18ij1zKknMkbbf2UXITIEeyMhIfRaF1exQLA/JRclslRykO+rs7s7s0VwytNmhJtsA8gHZEcox9PTs7uysQgGEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYr2OWRK/ReIKI8Zt7Hb19wTcQ0uTkGh13bQupcw7gPOvdo12/5CzNtNR7xLUtNtT3CGBQ6g3InjY720pvofUec22LJPr8PhEp2OMPyI40PdwWUdronCu9yQpdPx53bQlfLKnfOVhlnDYRBXve4Ov+IZTeMgdedm0NR+xoXJeQvdJ3CvziykSukwil16W/Oe7aGjIjqc/9ib4jQlJy0uArtN4A0+cvXFvDkmUJ47sJ1Y1ATLDNVXZkNPIepQzxy1ki9fqiwbUj/I+64zxWNzyZnPuhvohJ9K70VvXBixpcu2SAHU+Xd9EKdEJDNpYP3AQr3bQSpPQ6Y6/4dl1z7ZDbArsszjA7L0g7ibB0CDcidUWVoErvIMKZh2Xs0LUzcLW6V5NfiUgNEbaYmAVL6bXl0nJRc+1S72ua/D/cTjGPlQj7eUqd7A096rYlRjdPYlhz7VIvxpVG3cemDKF+WAwLY/6XelOZKTXXzsC4xvDjjtSN6kHLhLke6PrwM8h1raf40qjrGO7H9aTEbduucjS04ZrYU/4iuS5Z2Hdt0rvCLFdmLEXcU30AGddST62o+sLcf5l6k7CP+ru4pLYqX/VFyxbm/utQbx/r22ZEbTb2f5I2kns1Y1OQR8ZyofX+TjJxj1Rz7QQVnf1QzR26Oth0ueJVYcRP6ZUPac/Rx/5M6ixO1dhSrT3Y1DpiYmx3tF4ZUdpz9LD/dSg9PXES0LB71BwcGjKROuV28lnvnv7HHJsezheBGH5+X2CfSfRbMKW+5aGs3JFjMrjGibJc0S7TJzqjHrh2hDybj9XRXNZa89Aro55XBdbW5wti2c/5WJ7jJ1RolVUn/HWpb0I58Tziup6Rx7Dm2hnbRP1GM9PW/NFmQ4PtVRVN63Wvxfmu5sowDMMwDMMwDMMwDMMwDMMwDMMwzL+CpT//F/6beoV8zb2Jmt4Qryx6lTUCsENQ75HOkhXAO3EPVgyQtKtUy3C/e+FJg17Zjnew1Xrdb9InbG4WqfUAftG+WhLwPVyfg536+MU7m4C1CMk4ZznpXZzDYI1PDL2nS1hpvc5cNd7E2sJg05Fe7/7d3Fln8Cvc3bwB616auxsKl4WPghjemHrDqyDWeu1UNW5s2btPnSQ75oOdunEwWazfwgVG0kqluYCM9OIjWOGnfA2b9G4Ha63XKpvQ8perTvTifJNhi6+WMWmi7smEZf6G8MmhlyGq+NqP8GV84TLuJr7UIQVx+bDEoEpRZIz42gs40OuN4Mv8hXzelV7KX1isH+ewTWckikyVv+CfHuqVF7I16gN0VKypX6wPsE+zFPzkinolU9UH8OMGvSpnZqKsv13p/RsMun6X5x/y2LeAr8O66lsBwzBMP/wJfyGq8pgBk6IAAAAASUVORK5CYII='
'iVBORw0KGgoAAAANSUhEUgAAAV4AAACWBAMAAABkyf1EAAAAG1BMVEXMzMyWlpacnJyqqqrFxcWxsbGjo6O3t7e+vr6He3KoAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAEcElEQVR4nO2aTW/bRhCGh18ij1zKknMkbbf2UXITIEeyMhIfRaF1exQLA/JRclslRykO+rs7s7s0VwytNmhJtsA8gHZEcox9PTs7uysQgGEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYr2OWRK/ReIKI8Zt7Hb19wTcQ0uTkGh13bQupcw7gPOvdo12/5CzNtNR7xLUtNtT3CGBQ6g3InjY720pvofUec22LJPr8PhEp2OMPyI40PdwWUdronCu9yQpdPx53bQlfLKnfOVhlnDYRBXve4Ov+IZTeMgdedm0NR+xoXJeQvdJ3CvziykSukwil16W/Oe7aGjIjqc/9ib4jQlJy0uArtN4A0+cvXFvDkmUJ47sJ1Y1ATLDNVXZkNPIepQzxy1ki9fqiwbUj/I+64zxWNzyZnPuhvohJ9K70VvXBixpcu2SAHU+Xd9EKdEJDNpYP3AQr3bQSpPQ6Y6/4dl1z7ZDbArsszjA7L0g7ibB0CDcidUWVoErvIMKZh2Xs0LUzcLW6V5NfiUgNEbaYmAVL6bXl0nJRc+1S72ua/D/cTjGPlQj7eUqd7A096rYlRjdPYlhz7VIvxpVG3cemDKF+WAwLY/6XelOZKTXXzsC4xvDjjtSN6kHLhLke6PrwM8h1raf40qjrGO7H9aTEbduucjS04ZrYU/4iuS5Z2Hdt0rvCLFdmLEXcU30AGddST62o+sLcf5l6k7CP+ru4pLYqX/VFyxbm/utQbx/r22ZEbTb2f5I2kns1Y1OQR8ZyofX+TjJxj1Rz7QQVnf1QzR26Oth0ueJVYcRP6ZUPac/Rx/5M6ixO1dhSrT3Y1DpiYmx3tF4ZUdpz9LD/dSg9PXES0LB71BwcGjKROuV28lnvnv7HHJsezheBGH5+X2CfSfRbMKW+5aGs3JFjMrjGibJc0S7TJzqjHrh2hDybj9XRXNZa89Aro55XBdbW5wti2c/5WJ7jJ1RolVUn/HWpb0I58Tziup6Rx7Dm2hnbRP1GM9PW/NFmQ4PtVRVN63Wvxfmu5sowDMMwDMMwDMMwDMMwDMMwDMMwzL+CpT//F/6beoV8zb2Jmt4Qryx6lTUCsENQ75HOkhXAO3EPVgyQtKtUy3C/e+FJg17Zjnew1Xrdb9InbG4WqfUAftG+WhLwPVyfg536+MU7m4C1CMk4ZznpXZzDYI1PDL2nS1hpvc5cNd7E2sJg05Fe7/7d3Fln8Cvc3bwB616auxsKl4WPghjemHrDqyDWeu1UNW5s2btPnSQ75oOdunEwWazfwgVG0kqluYCM9OIjWOGnfA2b9G4Ha63XKpvQ8perTvTifJNhi6+WMWmi7smEZf6G8MmhlyGq+NqP8GV84TLuJr7UIQVx+bDEoEpRZIz42gs40OuN4Mv8hXzelV7KX1isH+ewTWckikyVv+CfHuqVF7I16gN0VKypX6wPsE+zFPzkinolU9UH8OMGvSpnZqKsv13p/RsMun6X5x/y2LeAr8O66lsBwzBMP/wJfyGq8pgBk6IAAAAASUVORK5CYII=',
);
const msg = await client.sendMessage(remoteId, media, {caption: 'here\'s my media'});
const msg = await client.sendMessage(remoteId, media, {
caption: "here's my media",
});
expect(msg).to.be.instanceOf(Message);
expect(msg.type).to.equal(MessageTypes.IMAGE);
expect(msg.fromMe).to.equal(true);
expect(msg.hasMedia).to.equal(true);
expect(msg.body).to.equal('here\'s my media');
expect(msg.body).to.equal("here's my media");
expect(msg.to).to.equal(remoteId);
});
it('can send a media message from URL', async function() {
const media = await MessageMedia.fromUrl('https://via.placeholder.com/350x150.png');
it('can send a media message from URL', async function () {
const media = await MessageMedia.fromUrl(
'https://via.placeholder.com/350x150.png',
);
const msg = await client.sendMessage(remoteId, media);
expect(msg).to.be.instanceOf(Message);
@ -201,14 +222,16 @@ describe('Client', function() {
expect(msg.to).to.equal(remoteId);
});
it('can send a media message as a document', async function() {
it('can send a media message as a document', async function () {
const media = new MessageMedia(
'image/png',
'iVBORw0KGgoAAAANSUhEUgAAAV4AAACWBAMAAABkyf1EAAAAG1BMVEXMzMyWlpacnJyqqqrFxcWxsbGjo6O3t7e+vr6He3KoAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAEcElEQVR4nO2aTW/bRhCGh18ij1zKknMkbbf2UXITIEeyMhIfRaF1exQLA/JRclslRykO+rs7s7s0VwytNmhJtsA8gHZEcox9PTs7uysQgGEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYr2OWRK/ReIKI8Zt7Hb19wTcQ0uTkGh13bQupcw7gPOvdo12/5CzNtNR7xLUtNtT3CGBQ6g3InjY720pvofUec22LJPr8PhEp2OMPyI40PdwWUdronCu9yQpdPx53bQlfLKnfOVhlnDYRBXve4Ov+IZTeMgdedm0NR+xoXJeQvdJ3CvziykSukwil16W/Oe7aGjIjqc/9ib4jQlJy0uArtN4A0+cvXFvDkmUJ47sJ1Y1ATLDNVXZkNPIepQzxy1ki9fqiwbUj/I+64zxWNzyZnPuhvohJ9K70VvXBixpcu2SAHU+Xd9EKdEJDNpYP3AQr3bQSpPQ6Y6/4dl1z7ZDbArsszjA7L0g7ibB0CDcidUWVoErvIMKZh2Xs0LUzcLW6V5NfiUgNEbaYmAVL6bXl0nJRc+1S72ua/D/cTjGPlQj7eUqd7A096rYlRjdPYlhz7VIvxpVG3cemDKF+WAwLY/6XelOZKTXXzsC4xvDjjtSN6kHLhLke6PrwM8h1raf40qjrGO7H9aTEbduucjS04ZrYU/4iuS5Z2Hdt0rvCLFdmLEXcU30AGddST62o+sLcf5l6k7CP+ru4pLYqX/VFyxbm/utQbx/r22ZEbTb2f5I2kns1Y1OQR8ZyofX+TjJxj1Rz7QQVnf1QzR26Oth0ueJVYcRP6ZUPac/Rx/5M6ixO1dhSrT3Y1DpiYmx3tF4ZUdpz9LD/dSg9PXES0LB71BwcGjKROuV28lnvnv7HHJsezheBGH5+X2CfSfRbMKW+5aGs3JFjMrjGibJc0S7TJzqjHrh2hDybj9XRXNZa89Aro55XBdbW5wti2c/5WJ7jJ1RolVUn/HWpb0I58Tziup6Rx7Dm2hnbRP1GM9PW/NFmQ4PtVRVN63Wvxfmu5sowDMMwDMMwDMMwDMMwDMMwDMMwzL+CpT//F/6beoV8zb2Jmt4Qryx6lTUCsENQ75HOkhXAO3EPVgyQtKtUy3C/e+FJg17Zjnew1Xrdb9InbG4WqfUAftG+WhLwPVyfg536+MU7m4C1CMk4ZznpXZzDYI1PDL2nS1hpvc5cNd7E2sJg05Fe7/7d3Fln8Cvc3bwB616auxsKl4WPghjemHrDqyDWeu1UNW5s2btPnSQ75oOdunEwWazfwgVG0kqluYCM9OIjWOGnfA2b9G4Ha63XKpvQ8perTvTifJNhi6+WMWmi7smEZf6G8MmhlyGq+NqP8GV84TLuJr7UIQVx+bDEoEpRZIz42gs40OuN4Mv8hXzelV7KX1isH+ewTWckikyVv+CfHuqVF7I16gN0VKypX6wPsE+zFPzkinolU9UH8OMGvSpnZqKsv13p/RsMun6X5x/y2LeAr8O66lsBwzBMP/wJfyGq8pgBk6IAAAAASUVORK5CYII=',
'this is my filename.png'
'this is my filename.png',
);
const msg = await client.sendMessage(remoteId, media, { sendMediaAsDocument: true});
const msg = await client.sendMessage(remoteId, media, {
sendMediaAsDocument: true,
});
expect(msg).to.be.instanceOf(Message);
expect(msg.type).to.equal(MessageTypes.DOCUMENT);
expect(msg.fromMe).to.equal(true);
@ -217,30 +240,14 @@ describe('Client', function() {
expect(msg.to).to.equal(remoteId);
});
it('can send a sticker message', async function() {
it('can send a sticker message', async function () {
const media = new MessageMedia(
'image/png',
'iVBORw0KGgoAAAANSUhEUgAAAV4AAACWBAMAAABkyf1EAAAAG1BMVEXMzMyWlpacnJyqqqrFxcWxsbGjo6O3t7e+vr6He3KoAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAEcElEQVR4nO2aTW/bRhCGh18ij1zKknMkbbf2UXITIEeyMhIfRaF1exQLA/JRclslRykO+rs7s7s0VwytNmhJtsA8gHZEcox9PTs7uysQgGEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYr2OWRK/ReIKI8Zt7Hb19wTcQ0uTkGh13bQupcw7gPOvdo12/5CzNtNR7xLUtNtT3CGBQ6g3InjY720pvofUec22LJPr8PhEp2OMPyI40PdwWUdronCu9yQpdPx53bQlfLKnfOVhlnDYRBXve4Ov+IZTeMgdedm0NR+xoXJeQvdJ3CvziykSukwil16W/Oe7aGjIjqc/9ib4jQlJy0uArtN4A0+cvXFvDkmUJ47sJ1Y1ATLDNVXZkNPIepQzxy1ki9fqiwbUj/I+64zxWNzyZnPuhvohJ9K70VvXBixpcu2SAHU+Xd9EKdEJDNpYP3AQr3bQSpPQ6Y6/4dl1z7ZDbArsszjA7L0g7ibB0CDcidUWVoErvIMKZh2Xs0LUzcLW6V5NfiUgNEbaYmAVL6bXl0nJRc+1S72ua/D/cTjGPlQj7eUqd7A096rYlRjdPYlhz7VIvxpVG3cemDKF+WAwLY/6XelOZKTXXzsC4xvDjjtSN6kHLhLke6PrwM8h1raf40qjrGO7H9aTEbduucjS04ZrYU/4iuS5Z2Hdt0rvCLFdmLEXcU30AGddST62o+sLcf5l6k7CP+ru4pLYqX/VFyxbm/utQbx/r22ZEbTb2f5I2kns1Y1OQR8ZyofX+TjJxj1Rz7QQVnf1QzR26Oth0ueJVYcRP6ZUPac/Rx/5M6ixO1dhSrT3Y1DpiYmx3tF4ZUdpz9LD/dSg9PXES0LB71BwcGjKROuV28lnvnv7HHJsezheBGH5+X2CfSfRbMKW+5aGs3JFjMrjGibJc0S7TJzqjHrh2hDybj9XRXNZa89Aro55XBdbW5wti2c/5WJ7jJ1RolVUn/HWpb0I58Tziup6Rx7Dm2hnbRP1GM9PW/NFmQ4PtVRVN63Wvxfmu5sowDMMwDMMwDMMwDMMwDMMwDMMwzL+CpT//F/6beoV8zb2Jmt4Qryx6lTUCsENQ75HOkhXAO3EPVgyQtKtUy3C/e+FJg17Zjnew1Xrdb9InbG4WqfUAftG+WhLwPVyfg536+MU7m4C1CMk4ZznpXZzDYI1PDL2nS1hpvc5cNd7E2sJg05Fe7/7d3Fln8Cvc3bwB616auxsKl4WPghjemHrDqyDWeu1UNW5s2btPnSQ75oOdunEwWazfwgVG0kqluYCM9OIjWOGnfA2b9G4Ha63XKpvQ8perTvTifJNhi6+WMWmi7smEZf6G8MmhlyGq+NqP8GV84TLuJr7UIQVx+bDEoEpRZIz42gs40OuN4Mv8hXzelV7KX1isH+ewTWckikyVv+CfHuqVF7I16gN0VKypX6wPsE+zFPzkinolU9UH8OMGvSpnZqKsv13p/RsMun6X5x/y2LeAr8O66lsBwzBMP/wJfyGq8pgBk6IAAAAASUVORK5CYII='
);
const msg = await client.sendMessage(remoteId, media, {sendMediaAsSticker: true});
expect(msg).to.be.instanceOf(Message);
expect(msg.type).to.equal(MessageTypes.STICKER);
expect(msg.fromMe).to.equal(true);
expect(msg.hasMedia).to.equal(true);
expect(msg.to).to.equal(remoteId);
});
it('can send a sticker message with custom author and name', async function() {
const media = new MessageMedia(
'image/png',
'iVBORw0KGgoAAAANSUhEUgAAAV4AAACWBAMAAABkyf1EAAAAG1BMVEXMzMyWlpacnJyqqqrFxcWxsbGjo6O3t7e+vr6He3KoAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAEcElEQVR4nO2aTW/bRhCGh18ij1zKknMkbbf2UXITIEeyMhIfRaF1exQLA/JRclslRykO+rs7s7s0VwytNmhJtsA8gHZEcox9PTs7uysQgGEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYr2OWRK/ReIKI8Zt7Hb19wTcQ0uTkGh13bQupcw7gPOvdo12/5CzNtNR7xLUtNtT3CGBQ6g3InjY720pvofUec22LJPr8PhEp2OMPyI40PdwWUdronCu9yQpdPx53bQlfLKnfOVhlnDYRBXve4Ov+IZTeMgdedm0NR+xoXJeQvdJ3CvziykSukwil16W/Oe7aGjIjqc/9ib4jQlJy0uArtN4A0+cvXFvDkmUJ47sJ1Y1ATLDNVXZkNPIepQzxy1ki9fqiwbUj/I+64zxWNzyZnPuhvohJ9K70VvXBixpcu2SAHU+Xd9EKdEJDNpYP3AQr3bQSpPQ6Y6/4dl1z7ZDbArsszjA7L0g7ibB0CDcidUWVoErvIMKZh2Xs0LUzcLW6V5NfiUgNEbaYmAVL6bXl0nJRc+1S72ua/D/cTjGPlQj7eUqd7A096rYlRjdPYlhz7VIvxpVG3cemDKF+WAwLY/6XelOZKTXXzsC4xvDjjtSN6kHLhLke6PrwM8h1raf40qjrGO7H9aTEbduucjS04ZrYU/4iuS5Z2Hdt0rvCLFdmLEXcU30AGddST62o+sLcf5l6k7CP+ru4pLYqX/VFyxbm/utQbx/r22ZEbTb2f5I2kns1Y1OQR8ZyofX+TjJxj1Rz7QQVnf1QzR26Oth0ueJVYcRP6ZUPac/Rx/5M6ixO1dhSrT3Y1DpiYmx3tF4ZUdpz9LD/dSg9PXES0LB71BwcGjKROuV28lnvnv7HHJsezheBGH5+X2CfSfRbMKW+5aGs3JFjMrjGibJc0S7TJzqjHrh2hDybj9XRXNZa89Aro55XBdbW5wti2c/5WJ7jJ1RolVUn/HWpb0I58Tziup6Rx7Dm2hnbRP1GM9PW/NFmQ4PtVRVN63Wvxfmu5sowDMMwDMMwDMMwDMMwDMMwDMMwzL+CpT//F/6beoV8zb2Jmt4Qryx6lTUCsENQ75HOkhXAO3EPVgyQtKtUy3C/e+FJg17Zjnew1Xrdb9InbG4WqfUAftG+WhLwPVyfg536+MU7m4C1CMk4ZznpXZzDYI1PDL2nS1hpvc5cNd7E2sJg05Fe7/7d3Fln8Cvc3bwB616auxsKl4WPghjemHrDqyDWeu1UNW5s2btPnSQ75oOdunEwWazfwgVG0kqluYCM9OIjWOGnfA2b9G4Ha63XKpvQ8perTvTifJNhi6+WMWmi7smEZf6G8MmhlyGq+NqP8GV84TLuJr7UIQVx+bDEoEpRZIz42gs40OuN4Mv8hXzelV7KX1isH+ewTWckikyVv+CfHuqVF7I16gN0VKypX6wPsE+zFPzkinolU9UH8OMGvSpnZqKsv13p/RsMun6X5x/y2LeAr8O66lsBwzBMP/wJfyGq8pgBk6IAAAAASUVORK5CYII='
'iVBORw0KGgoAAAANSUhEUgAAAV4AAACWBAMAAABkyf1EAAAAG1BMVEXMzMyWlpacnJyqqqrFxcWxsbGjo6O3t7e+vr6He3KoAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAEcElEQVR4nO2aTW/bRhCGh18ij1zKknMkbbf2UXITIEeyMhIfRaF1exQLA/JRclslRykO+rs7s7s0VwytNmhJtsA8gHZEcox9PTs7uysQgGEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYr2OWRK/ReIKI8Zt7Hb19wTcQ0uTkGh13bQupcw7gPOvdo12/5CzNtNR7xLUtNtT3CGBQ6g3InjY720pvofUec22LJPr8PhEp2OMPyI40PdwWUdronCu9yQpdPx53bQlfLKnfOVhlnDYRBXve4Ov+IZTeMgdedm0NR+xoXJeQvdJ3CvziykSukwil16W/Oe7aGjIjqc/9ib4jQlJy0uArtN4A0+cvXFvDkmUJ47sJ1Y1ATLDNVXZkNPIepQzxy1ki9fqiwbUj/I+64zxWNzyZnPuhvohJ9K70VvXBixpcu2SAHU+Xd9EKdEJDNpYP3AQr3bQSpPQ6Y6/4dl1z7ZDbArsszjA7L0g7ibB0CDcidUWVoErvIMKZh2Xs0LUzcLW6V5NfiUgNEbaYmAVL6bXl0nJRc+1S72ua/D/cTjGPlQj7eUqd7A096rYlRjdPYlhz7VIvxpVG3cemDKF+WAwLY/6XelOZKTXXzsC4xvDjjtSN6kHLhLke6PrwM8h1raf40qjrGO7H9aTEbduucjS04ZrYU/4iuS5Z2Hdt0rvCLFdmLEXcU30AGddST62o+sLcf5l6k7CP+ru4pLYqX/VFyxbm/utQbx/r22ZEbTb2f5I2kns1Y1OQR8ZyofX+TjJxj1Rz7QQVnf1QzR26Oth0ueJVYcRP6ZUPac/Rx/5M6ixO1dhSrT3Y1DpiYmx3tF4ZUdpz9LD/dSg9PXES0LB71BwcGjKROuV28lnvnv7HHJsezheBGH5+X2CfSfRbMKW+5aGs3JFjMrjGibJc0S7TJzqjHrh2hDybj9XRXNZa89Aro55XBdbW5wti2c/5WJ7jJ1RolVUn/HWpb0I58Tziup6Rx7Dm2hnbRP1GM9PW/NFmQ4PtVRVN63Wvxfmu5sowDMMwDMMwDMMwDMMwDMMwDMMwzL+CpT//F/6beoV8zb2Jmt4Qryx6lTUCsENQ75HOkhXAO3EPVgyQtKtUy3C/e+FJg17Zjnew1Xrdb9InbG4WqfUAftG+WhLwPVyfg536+MU7m4C1CMk4ZznpXZzDYI1PDL2nS1hpvc5cNd7E2sJg05Fe7/7d3Fln8Cvc3bwB616auxsKl4WPghjemHrDqyDWeu1UNW5s2btPnSQ75oOdunEwWazfwgVG0kqluYCM9OIjWOGnfA2b9G4Ha63XKpvQ8perTvTifJNhi6+WMWmi7smEZf6G8MmhlyGq+NqP8GV84TLuJr7UIQVx+bDEoEpRZIz42gs40OuN4Mv8hXzelV7KX1isH+ewTWckikyVv+CfHuqVF7I16gN0VKypX6wPsE+zFPzkinolU9UH8OMGvSpnZqKsv13p/RsMun6X5x/y2LeAr8O66lsBwzBMP/wJfyGq8pgBk6IAAAAASUVORK5CYII=',
);
const msg = await client.sendMessage(remoteId, media, {
sendMediaAsSticker: true,
stickerAuthor: 'WWEBJS',
stickerName: 'My Sticker'
});
expect(msg).to.be.instanceOf(Message);
expect(msg.type).to.equal(MessageTypes.STICKER);
@ -249,8 +256,30 @@ describe('Client', function() {
expect(msg.to).to.equal(remoteId);
});
it('can send a location message', async function() {
const location = new Location(37.422, -122.084, 'Googleplex\nGoogle Headquarters');
it('can send a sticker message with custom author and name', async function () {
const media = new MessageMedia(
'image/png',
'iVBORw0KGgoAAAANSUhEUgAAAV4AAACWBAMAAABkyf1EAAAAG1BMVEXMzMyWlpacnJyqqqrFxcWxsbGjo6O3t7e+vr6He3KoAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAEcElEQVR4nO2aTW/bRhCGh18ij1zKknMkbbf2UXITIEeyMhIfRaF1exQLA/JRclslRykO+rs7s7s0VwytNmhJtsA8gHZEcox9PTs7uysQgGEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYr2OWRK/ReIKI8Zt7Hb19wTcQ0uTkGh13bQupcw7gPOvdo12/5CzNtNR7xLUtNtT3CGBQ6g3InjY720pvofUec22LJPr8PhEp2OMPyI40PdwWUdronCu9yQpdPx53bQlfLKnfOVhlnDYRBXve4Ov+IZTeMgdedm0NR+xoXJeQvdJ3CvziykSukwil16W/Oe7aGjIjqc/9ib4jQlJy0uArtN4A0+cvXFvDkmUJ47sJ1Y1ATLDNVXZkNPIepQzxy1ki9fqiwbUj/I+64zxWNzyZnPuhvohJ9K70VvXBixpcu2SAHU+Xd9EKdEJDNpYP3AQr3bQSpPQ6Y6/4dl1z7ZDbArsszjA7L0g7ibB0CDcidUWVoErvIMKZh2Xs0LUzcLW6V5NfiUgNEbaYmAVL6bXl0nJRc+1S72ua/D/cTjGPlQj7eUqd7A096rYlRjdPYlhz7VIvxpVG3cemDKF+WAwLY/6XelOZKTXXzsC4xvDjjtSN6kHLhLke6PrwM8h1raf40qjrGO7H9aTEbduucjS04ZrYU/4iuS5Z2Hdt0rvCLFdmLEXcU30AGddST62o+sLcf5l6k7CP+ru4pLYqX/VFyxbm/utQbx/r22ZEbTb2f5I2kns1Y1OQR8ZyofX+TjJxj1Rz7QQVnf1QzR26Oth0ueJVYcRP6ZUPac/Rx/5M6ixO1dhSrT3Y1DpiYmx3tF4ZUdpz9LD/dSg9PXES0LB71BwcGjKROuV28lnvnv7HHJsezheBGH5+X2CfSfRbMKW+5aGs3JFjMrjGibJc0S7TJzqjHrh2hDybj9XRXNZa89Aro55XBdbW5wti2c/5WJ7jJ1RolVUn/HWpb0I58Tziup6Rx7Dm2hnbRP1GM9PW/NFmQ4PtVRVN63Wvxfmu5sowDMMwDMMwDMMwDMMwDMMwDMMwzL+CpT//F/6beoV8zb2Jmt4Qryx6lTUCsENQ75HOkhXAO3EPVgyQtKtUy3C/e+FJg17Zjnew1Xrdb9InbG4WqfUAftG+WhLwPVyfg536+MU7m4C1CMk4ZznpXZzDYI1PDL2nS1hpvc5cNd7E2sJg05Fe7/7d3Fln8Cvc3bwB616auxsKl4WPghjemHrDqyDWeu1UNW5s2btPnSQ75oOdunEwWazfwgVG0kqluYCM9OIjWOGnfA2b9G4Ha63XKpvQ8perTvTifJNhi6+WMWmi7smEZf6G8MmhlyGq+NqP8GV84TLuJr7UIQVx+bDEoEpRZIz42gs40OuN4Mv8hXzelV7KX1isH+ewTWckikyVv+CfHuqVF7I16gN0VKypX6wPsE+zFPzkinolU9UH8OMGvSpnZqKsv13p/RsMun6X5x/y2LeAr8O66lsBwzBMP/wJfyGq8pgBk6IAAAAASUVORK5CYII=',
);
const msg = await client.sendMessage(remoteId, media, {
sendMediaAsSticker: true,
stickerAuthor: 'WWEBJS',
stickerName: 'My Sticker',
});
expect(msg).to.be.instanceOf(Message);
expect(msg.type).to.equal(MessageTypes.STICKER);
expect(msg.fromMe).to.equal(true);
expect(msg.hasMedia).to.equal(true);
expect(msg.to).to.equal(remoteId);
});
it('can send a location message', async function () {
const location = new Location(
37.422,
-122.084,
'Googleplex\nGoogle Headquarters',
);
const msg = await client.sendMessage(remoteId, location);
expect(msg).to.be.instanceOf(Message);
@ -261,10 +290,12 @@ describe('Client', function() {
expect(msg.location).to.be.instanceOf(Location);
expect(msg.location.latitude).to.equal(37.422);
expect(msg.location.longitude).to.equal(-122.084);
expect(msg.location.description).to.equal('Googleplex\nGoogle Headquarters');
expect(msg.location.description).to.equal(
'Googleplex\nGoogle Headquarters',
);
});
it('can send a vCard as a contact card message', async function() {
it('can send a vCard as a contact card message', async function () {
const vCard = `BEGIN:VCARD
VERSION:3.0
FN;CHARSET=UTF-8:John Doe
@ -284,7 +315,7 @@ END:VCARD`;
expect(msg.vCards[0]).to.equal(vCard);
});
it('can optionally turn off vCard parsing', async function() {
it('can optionally turn off vCard parsing', async function () {
const vCard = `BEGIN:VCARD
VERSION:3.0
FN;CHARSET=UTF-8:John Doe
@ -294,7 +325,9 @@ TEL;TYPE=HOME,VOICE:1234567890
REV:2021-06-06T02:35:53.559Z
END:VCARD`;
const msg = await client.sendMessage(remoteId, vCard, {parseVCards: false});
const msg = await client.sendMessage(remoteId, vCard, {
parseVCards: false,
});
expect(msg).to.be.instanceOf(Message);
expect(msg.type).to.equal(MessageTypes.TEXT); // not a contact card
expect(msg.fromMe).to.equal(true);
@ -302,7 +335,7 @@ END:VCARD`;
expect(msg.body).to.equal(vCard);
});
it('can send a Contact as a contact card message', async function() {
it('can send a Contact as a contact card message', async function () {
const contact = await client.getContactById(remoteId);
const msg = await client.sendMessage(remoteId, contact);
@ -317,9 +350,13 @@ END:VCARD`;
it('can send multiple Contacts as a contact card message', async function () {
const contact1 = await client.getContactById(remoteId);
const contact2 = await client.getContactById('5511942167462@c.us'); //iFood
const contact2 =
await client.getContactById('5511942167462@c.us'); //iFood
const msg = await client.sendMessage(remoteId, [contact1, contact2]);
const msg = await client.sendMessage(remoteId, [
contact1,
contact2,
]);
expect(msg).to.be.instanceOf(Message);
expect(msg.type).to.equal(MessageTypes.CONTACT_CARD_MULTI);
expect(msg.fromMe).to.equal(true);
@ -342,7 +379,7 @@ END:VCARD`;
const chats = await client.getChats();
expect(chats.length).to.be.greaterThanOrEqual(1);
const chat = chats.find(c => c.id._serialized === remoteId);
const chat = chats.find((c) => c.id._serialized === remoteId);
expect(chat).to.exist;
expect(chat).to.be.instanceOf(Chat);
});
@ -360,7 +397,9 @@ END:VCARD`;
const contacts = await client.getContacts();
expect(contacts.length).to.be.greaterThanOrEqual(1);
const contact = contacts.find(c => c.id._serialized === remoteId);
const contact = contacts.find(
(c) => c.id._serialized === remoteId,
);
expect(contact).to.exist;
expect(contact).to.be.instanceOf(Contact);
});
@ -377,10 +416,11 @@ END:VCARD`;
const blockedContacts = await client.getBlockedContacts();
expect(blockedContacts.length).to.be.greaterThanOrEqual(1);
const contact = blockedContacts.find(c => c.id._serialized === remoteId);
const contact = blockedContacts.find(
(c) => c.id._serialized === remoteId,
);
expect(contact).to.exist;
expect(contact).to.be.instanceOf(Contact);
});
it('can unblock a contact', async function () {
@ -399,27 +439,28 @@ END:VCARD`;
});
it('can verify that a user is not registered', async function () {
const isRegistered = await client.isRegisteredUser('9999999999@c.us');
const isRegistered =
await client.isRegisteredUser('9999999999@c.us');
expect(isRegistered).to.be.false;
});
it('can get a number\'s whatsapp id', async function () {
it("can get a number's whatsapp id", async function () {
const number = remoteId.split('@')[0];
const numberId = await client.getNumberId(number);
expect(numberId).to.eql({
server: 'c.us',
user: number,
_serialized: `${number}@c.us`
_serialized: `${number}@c.us`,
});
});
it('returns null when getting an unregistered number\'s whatsapp id', async function () {
it("returns null when getting an unregistered number's whatsapp id", async function () {
const number = '9999999999';
const numberId = await client.getNumberId(number);
expect(numberId).to.eql(null);
});
it('can get a number\'s country code', async function () {
it("can get a number's country code", async function () {
const number = '18092201111';
const countryCode = await client.getCountryCode(number);
expect(countryCode).to.eql('1');
@ -440,18 +481,30 @@ END:VCARD`;
describe('Search messages', function () {
it('can search for messages', async function () {
const m1 = await client.sendMessage(remoteId, 'I\'m searching for Super Mario Brothers');
const m2 = await client.sendMessage(remoteId, 'This also contains Mario');
const m3 = await client.sendMessage(remoteId, 'Nothing of interest here, just Luigi');
const m1 = await client.sendMessage(
remoteId,
"I'm searching for Super Mario Brothers",
);
const m2 = await client.sendMessage(
remoteId,
'This also contains Mario',
);
const m3 = await client.sendMessage(
remoteId,
'Nothing of interest here, just Luigi',
);
// wait for search index to catch up
await helper.sleep(1000);
const msgs = await client.searchMessages('Mario', {chatId: remoteId});
const msgs = await client.searchMessages('Mario', {
chatId: remoteId,
});
expect(msgs.length).to.be.greaterThanOrEqual(2);
const msgIds = msgs.map(m => m.id._serialized);
const msgIds = msgs.map((m) => m.id._serialized);
expect(msgIds).to.include.members([
m1.id._serialized, m2.id._serialized
m1.id._serialized,
m2.id._serialized,
]);
expect(msgIds).to.not.include.members([m3.id._serialized]);
});

View file

@ -3,26 +3,28 @@ const { Client, LocalAuth } = require('..');
require('dotenv').config();
const remoteId = process.env.WWEBJS_TEST_REMOTE_ID;
if(!remoteId) throw new Error('The WWEBJS_TEST_REMOTE_ID environment variable has not been set.');
if (!remoteId)
throw new Error(
'The WWEBJS_TEST_REMOTE_ID environment variable has not been set.',
);
function createClient({authenticated, options: additionalOpts}={}) {
function createClient({ authenticated, options: additionalOpts } = {}) {
const options = {};
if(authenticated) {
if (authenticated) {
const clientId = process.env.WWEBJS_TEST_CLIENT_ID;
if(!clientId) throw new Error('No session found in environment.');
if (!clientId) throw new Error('No session found in environment.');
options.authStrategy = new LocalAuth({
clientId
clientId,
});
}
const allOpts = {...options, ...(additionalOpts || {})};
const allOpts = { ...options, ...(additionalOpts || {}) };
return new Client(allOpts);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
module.exports = {

View file

@ -11,7 +11,7 @@ describe('Chat', function () {
let client;
let chat;
before(async function() {
before(async function () {
this.timeout(35000);
client = helper.createClient({ authenticated: true });
await client.initialize();
@ -39,7 +39,7 @@ describe('Chat', function () {
const messages = await chat.fetchMessages();
expect(messages.length).to.be.greaterThanOrEqual(2);
const fetchedMsg = messages[messages.length-1];
const fetchedMsg = messages[messages.length - 1];
expect(fetchedMsg).to.be.instanceOf(Message);
expect(fetchedMsg.type).to.equal(MessageTypes.TEXT);
expect(fetchedMsg.id._serialized).to.equal(msg.id._serialized);
@ -51,7 +51,7 @@ describe('Chat', function () {
const msg = await chat.sendMessage('yet another message');
await helper.sleep(500);
const messages = await chat.fetchMessages({limit: 1});
const messages = await chat.fetchMessages({ limit: 1 });
expect(messages).to.have.lengthOf(1);
const fetchedMsg = messages[0];
@ -62,12 +62,12 @@ describe('Chat', function () {
});
it('can use fromMe=true when fetching messages sent in a chat to get only bot messages', async function () {
const messages = await chat.fetchMessages({fromMe: true});
const messages = await chat.fetchMessages({ fromMe: true });
expect(messages).to.have.lengthOf(2);
});
it('can use fromMe=false when fetching messages sent in a chat to get only non bot messages', async function () {
const messages = await chat.fetchMessages({fromMe: false});
const messages = await chat.fetchMessages({ fromMe: false });
expect(messages).to.have.lengthOf(0);
});
@ -99,7 +99,7 @@ describe('Chat', function () {
});
});
describe('Archiving', function (){
describe('Archiving', function () {
it('can archive a chat', async function () {
const res = await chat.archive();
expect(res).to.equal(true);
@ -147,7 +147,7 @@ describe('Chat', function () {
});
describe('Muting', function () {
it('can mute a chat forever', async function() {
it('can mute a chat forever', async function () {
await chat.mute();
await helper.sleep(1000);
@ -158,8 +158,8 @@ describe('Chat', function () {
expect(chat.muteExpiration).to.equal(-1);
});
it('can mute a chat until a specific date', async function() {
const unmuteDate = new Date(new Date().getTime() + (1000*60*60));
it('can mute a chat until a specific date', async function () {
const unmuteDate = new Date(new Date().getTime() + 1000 * 60 * 60);
await chat.mute(unmuteDate);
await helper.sleep(1000);
@ -168,7 +168,7 @@ describe('Chat', function () {
chat = await client.getChatById(remoteId);
expect(chat.isMuted).to.equal(true);
expect(chat.muteExpiration).to.equal(
Math.round(unmuteDate.getTime() / 1000)
Math.round(unmuteDate.getTime() / 1000),
);
});

View file

@ -3,18 +3,20 @@ const helper = require('../helper');
const remoteId = helper.remoteId;
describe('Group', function() {
describe('Group', function () {
let client;
let group;
before(async function() {
before(async function () {
this.timeout(35000);
client = helper.createClient({
authenticated: true,
});
await client.initialize();
const createRes = await client.createGroup('My Awesome Group', [remoteId]);
const createRes = await client.createGroup('My Awesome Group', [
remoteId,
]);
expect(createRes.gid).to.exist;
await helper.sleep(500);
group = await client.getChatById(createRes.gid._serialized);
@ -127,7 +129,9 @@ describe('Group', function() {
describe('Participants', function () {
it('can promote a user to admin', async function () {
let participant = group.participants.find(p => p.id._serialized === remoteId);
let participant = group.participants.find(
(p) => p.id._serialized === remoteId,
);
expect(participant).to.exist;
expect(participant.isAdmin).to.equal(false);
@ -138,13 +142,17 @@ describe('Group', function() {
// reload and check
group = await client.getChatById(group.id._serialized);
participant = group.participants.find(p => p.id._serialized=== remoteId);
participant = group.participants.find(
(p) => p.id._serialized === remoteId,
);
expect(participant).to.exist;
expect(participant.isAdmin).to.equal(true);
});
it('can demote a user', async function () {
let participant = group.participants.find(p => p.id._serialized=== remoteId);
let participant = group.participants.find(
(p) => p.id._serialized === remoteId,
);
expect(participant).to.exist;
expect(participant.isAdmin).to.equal(true);
@ -155,13 +163,17 @@ describe('Group', function() {
// reload and check
group = await client.getChatById(group.id._serialized);
participant = group.participants.find(p => p.id._serialized=== remoteId);
participant = group.participants.find(
(p) => p.id._serialized === remoteId,
);
expect(participant).to.exist;
expect(participant.isAdmin).to.equal(false);
});
it('can remove a user from the group', async function () {
let participant = group.participants.find(p => p.id._serialized=== remoteId);
let participant = group.participants.find(
(p) => p.id._serialized === remoteId,
);
expect(participant).to.exist;
const res = await group.removeParticipants([remoteId]);
@ -171,12 +183,16 @@ describe('Group', function() {
// reload and check
group = await client.getChatById(group.id._serialized);
participant = group.participants.find(p => p.id._serialized=== remoteId);
participant = group.participants.find(
(p) => p.id._serialized === remoteId,
);
expect(participant).to.not.exist;
});
it('can add back a user to the group', async function () {
let participant = group.participants.find(p => p.id._serialized=== remoteId);
let participant = group.participants.find(
(p) => p.id._serialized === remoteId,
);
expect(participant).to.not.exist;
const res = await group.addParticipants([remoteId]);
@ -186,7 +202,9 @@ describe('Group', function() {
// reload and check
group = await client.getChatById(group.id._serialized);
participant = group.participants.find(p => p.id._serialized=== remoteId);
participant = group.participants.find(
(p) => p.id._serialized === remoteId,
);
expect(participant).to.exist;
});
});
@ -224,5 +242,4 @@ describe('Group', function() {
after(async function () {
await client.destroy();
});
});

View file

@ -11,7 +11,7 @@ describe('Message', function () {
let chat;
let message;
before(async function() {
before(async function () {
this.timeout(35000);
client = helper.createClient({ authenticated: true });
await client.initialize();
@ -105,8 +105,10 @@ describe('Message', function () {
expect(await message.reload()).to.equal(null);
expect(callback.called).to.equal(true);
const [ revokeMsg, originalMsg ] = callback.args[0];
expect(revokeMsg.id._serialized).to.equal(originalMsg.id._serialized);
const [revokeMsg, originalMsg] = callback.args[0];
expect(revokeMsg.id._serialized).to.equal(
originalMsg.id._serialized,
);
expect(originalMsg.body).to.equal('sneaky message');
expect(originalMsg.type).to.equal('chat');
expect(revokeMsg.body).to.equal('');

View file

@ -42,12 +42,12 @@ const args = [
'--disable-setuid-sandbox',
'--no-sandbox',
'--disable-blink-features=AutomationControlled',
'--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36'
'--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
];
const getLatestVersion = async () => {
const browser = await puppeteer.launch({
args: args
args: args,
});
const page = await browser.newPage();
await page.goto('https://web.whatsapp.com', {
@ -56,7 +56,9 @@ const getLatestVersion = async () => {
referer: 'https://whatsapp.com/',
});
await page.waitForFunction(() => window.require && window.require('WAWebBuildConstants'));
await page.waitForFunction(
() => window.require && window.require('WAWebBuildConstants'),
);
const version = await page.evaluate(() => {
return window.require('WAWebBuildConstants').VERSION_STR;
@ -70,7 +72,7 @@ const getCurrentVersion = () => {
try {
const versionFile = fs.readFileSync('./.version');
return versionFile ? versionFile.toString().trim() : null;
} catch(_) {
} catch (_) {
return null;
}
};
@ -83,10 +85,7 @@ const updateInFile = (filePath, oldVersion, newVersion) => {
};
const updateVersion = async (oldVersion, newVersion) => {
const filesToUpdate = [
'../../src/util/Constants.js',
'../../README.md',
];
const filesToUpdate = ['../../src/util/Constants.js', '../../README.md'];
for (const file of filesToUpdate) {
updateInFile(file, oldVersion, newVersion);