├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── package.json ├── shell.js ├── src ├── Client.js ├── authStrategies │ ├── BaseAuthStrategy.js │ ├── LegacySessionAuth.js │ ├── LinkingMethod.js │ ├── LocalAuth.js │ ├── NoAuth.js │ └── RemoteAuth.js ├── factories │ ├── ChatFactory.js │ └── ContactFactory.js ├── structures │ ├── Base.js │ ├── BusinessContact.js │ ├── Buttons.js │ ├── Call.js │ ├── Chat.js │ ├── ClientInfo.js │ ├── Contact.js │ ├── GroupChat.js │ ├── GroupNotification.js │ ├── Label.js │ ├── List.js │ ├── Location.js │ ├── Message.js │ ├── MessageMedia.js │ ├── Order.js │ ├── Payment.js │ ├── Poll.js │ ├── PrivateChat.js │ ├── PrivateContact.js │ ├── Product.js │ ├── ProductMetadata.js │ ├── Reaction.js │ └── index.js ├── util │ ├── Constants.js │ ├── Injected.js │ ├── InterfaceController.js │ └── Util.js └── webCache │ ├── LocalWebCache.js │ ├── RemoteWebCache.js │ ├── WebCache.js │ └── WebCacheFactory.js ├── tests ├── README.md ├── client.js ├── helper.js └── structures │ ├── chat.js │ ├── group.js │ └── message.js └── tools ├── changelog.sh ├── publish └── version-checker ├── .version └── update-version /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = false 8 | insert_final_newline = false -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | WWEBJS_TEST_REMOTE_ID=XXXXXXXXXX@c.us 2 | WWEBJS_TEST_CLIENT_ID=authenticated 3 | WWEBJS_TEST_MD=1 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": ["eslint:recommended", "plugin:mocha/recommended"], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2020 15 | }, 16 | "plugins": ["mocha"], 17 | "ignorePatterns": ["docs"], 18 | "rules": { 19 | "indent": [ 20 | "error", 21 | 4 22 | ], 23 | "linebreak-style": [ 24 | "error", 25 | "unix" 26 | ], 27 | "quotes": [ 28 | "error", 29 | "single" 30 | ], 31 | "semi": [ 32 | "error", 33 | "always" 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Lock files 40 | package-lock.json 41 | yarn.lock 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | 67 | # macOS 68 | ._* 69 | .DS_Store 70 | 71 | # Test sessions 72 | *session.json 73 | .wwebjs_auth/ 74 | 75 | # local version cache 76 | .wwebjs_cache/ 77 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/* 2 | .github/* 3 | 4 | .eslintrc.json 5 | .jsdoc.json 6 | .editorconfig 7 | 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | *session.json 15 | .wwebjs_auth/ 16 | .wwebjs_cache/ 17 | 18 | .env 19 | tools/ 20 | tests/ 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [pedroslopez@me.com](mailto:pedroslopez@me.com). 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Pedro S Lopez 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![-----------------------------------------------------](https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/colored.png)](#table-of-contents) 2 | # MYWAJS 3 | [JOIN GROUP](https://chat.whatsapp.com/Gkl0LlOd70J1W1VKALiIJt) 4 | 5 | 6 | ## This project is closed, and moved to [CLICK HERE](https://github.com/DikaArdnt/wajs/tree/master) 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Constants = require('./src/util/Constants'); 4 | 5 | module.exports = { 6 | Client: require('./src/Client'), 7 | 8 | version: require('./package.json').version, 9 | 10 | // Structures 11 | Chat: require('./src/structures/Chat'), 12 | PrivateChat: require('./src/structures/PrivateChat'), 13 | GroupChat: require('./src/structures/GroupChat'), 14 | Message: require('./src/structures/Message'), 15 | MessageMedia: require('./src/structures/MessageMedia'), 16 | Contact: require('./src/structures/Contact'), 17 | PrivateContact: require('./src/structures/PrivateContact'), 18 | BusinessContact: require('./src/structures/BusinessContact'), 19 | ClientInfo: require('./src/structures/ClientInfo'), 20 | Location: require('./src/structures/Location'), 21 | Poll: require('./src/structures/Poll'), 22 | ProductMetadata: require('./src/structures/ProductMetadata'), 23 | List: require('./src/structures/List'), 24 | Buttons: require('./src/structures/Buttons'), 25 | 26 | // Auth Strategies 27 | LinkingMethod: require('./src/authStrategies/LinkingMethod'), 28 | NoAuth: require('./src/authStrategies/NoAuth'), 29 | LocalAuth: require('./src/authStrategies/LocalAuth'), 30 | RemoteAuth: require('./src/authStrategies/RemoteAuth'), 31 | LegacySessionAuth: require('./src/authStrategies/LegacySessionAuth'), 32 | 33 | ...Constants 34 | }; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mywajs", 3 | "version": "2.0.9", 4 | "description": "redeveloped wwebjs with wa-js using playwright", 5 | "main": "./index.js", 6 | "typings": "./index.d.ts", 7 | "scripts": { 8 | "test": "mocha tests --recursive --timeout 5000", 9 | "test-single": "mocha", 10 | "shell": "node --experimental-repl-await ./shell.js", 11 | "generate-docs": "node_modules/.bin/jsdoc --configure .jsdoc.json --verbose" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/amiruldev20/mywajs.git" 16 | }, 17 | "keywords": [ 18 | "whatsapp", 19 | "whatsapp-web", 20 | "api", 21 | "bot", 22 | "client", 23 | "node", 24 | "baileys", 25 | "amiruldev", 26 | "mywajs" 27 | ], 28 | "author": "Amirul Dev", 29 | "license": "Apache-2.0", 30 | "bugs": { 31 | "url": "https://github.com/amiruldev20/mywajs/issues" 32 | }, 33 | "homepage": "https://github.com/amiruldev20/", 34 | "dependencies": { 35 | "@pedroslopez/moduleraid": "^5.0.2", 36 | "@amiruldev/wajs": "github:amiruldev20/wajs", 37 | "fluent-ffmpeg": "^2.1.2", 38 | "file-type": "^16.5.3", 39 | "jsqr": "^1.3.1", 40 | "mime": "^3.0.0", 41 | "node-fetch": "^2.6.5", 42 | "node-webpmux": "^3.1.0", 43 | "playwright-chromium": "latest", 44 | "qrcode-terminal": "0.12.0", 45 | "sharp": "latest" 46 | }, 47 | "devDependencies": { 48 | "@types/node-fetch": "^2.5.12", 49 | "chai": "^4.3.4", 50 | "chai-as-promised": "^7.1.1", 51 | "dotenv": "^16.0.0", 52 | "eslint": "^8.4.1", 53 | "eslint-plugin-mocha": "^10.0.3", 54 | "jsdoc": "^3.6.4", 55 | "jsdoc-baseline": "^0.1.5", 56 | "mocha": "^9.0.2", 57 | "sinon": "^13.0.1" 58 | }, 59 | "engines": { 60 | "node": ">=12.0.0" 61 | }, 62 | "optionalDependencies": { 63 | "archiver": "^5.3.1", 64 | "fs-extra": "^10.1.0", 65 | "unzipper": "^0.10.11" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /shell.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ==== wwebjs-shell ==== 3 | * Used for quickly testing library features 4 | * 5 | * Running `npm run shell` will start WhatsApp Web with headless=false 6 | * and then drop you into Node REPL with `client` in its context. 7 | */ 8 | 9 | const repl = require('repl'); 10 | 11 | const { Client, LocalAuth } = require('./index'); 12 | 13 | const client = new Client({ 14 | puppeteer: { headless: false }, 15 | authStrategy: new LocalAuth() 16 | }); 17 | 18 | console.log('Initializing...'); 19 | 20 | client.initialize(); 21 | 22 | client.on('qr', () => { 23 | console.log('Please scan the QR code on the browser.'); 24 | }); 25 | 26 | client.on('authenticated', (session) => { 27 | console.log(JSON.stringify(session)); 28 | }); 29 | 30 | client.on('ready', () => { 31 | const shell = repl.start('wwebjs> '); 32 | shell.context.client = client; 33 | shell.on('exit', async () => { 34 | await client.destroy(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/authStrategies/BaseAuthStrategy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Base class which all authentication strategies extend 5 | */ 6 | class BaseAuthStrategy { 7 | constructor() {} 8 | setup(client) { 9 | this.client = client; 10 | } 11 | async beforeBrowserInitialized() {} 12 | async afterBrowserInitialized() {} 13 | async onAuthenticationNeeded() { 14 | return { 15 | failed: false, 16 | restart: false, 17 | failureEventPayload: undefined 18 | }; 19 | } 20 | async getAuthEventPayload() {} 21 | async afterAuthReady() {} 22 | async disconnect() {} 23 | async destroy() {} 24 | async logout() {} 25 | } 26 | 27 | module.exports = BaseAuthStrategy; -------------------------------------------------------------------------------- /src/authStrategies/LegacySessionAuth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BaseAuthStrategy = require('./BaseAuthStrategy'); 4 | 5 | /** 6 | * Legacy session auth strategy 7 | * Not compatible with multi-device accounts. 8 | * @param {object} options - options 9 | * @param {string} options.restartOnAuthFail - Restart client with a new session (i.e. use null 'session' var) if authentication fails 10 | * @param {object} options.session - Whatsapp session to restore. If not set, will start a new session 11 | * @param {string} options.session.WABrowserId 12 | * @param {string} options.session.WASecretBundle 13 | * @param {string} options.session.WAToken1 14 | * @param {string} options.session.WAToken2 15 | */ 16 | class LegacySessionAuth extends BaseAuthStrategy { 17 | constructor({ session, restartOnAuthFail }={}) { 18 | super(); 19 | this.session = session; 20 | this.restartOnAuthFail = restartOnAuthFail; 21 | } 22 | 23 | async afterBrowserInitialized() { 24 | if(this.session) { 25 | await this.client.mPage.evaluateOnNewDocument(session => { 26 | if (document.referrer === 'https://whatsapp.com/') { 27 | localStorage.clear(); 28 | localStorage.setItem('WABrowserId', session.WABrowserId); 29 | localStorage.setItem('WASecretBundle', session.WASecretBundle); 30 | localStorage.setItem('WAToken1', session.WAToken1); 31 | localStorage.setItem('WAToken2', session.WAToken2); 32 | } 33 | 34 | localStorage.setItem('remember-me', 'true'); 35 | }, this.session); 36 | } 37 | } 38 | 39 | async onAuthenticationNeeded() { 40 | if(this.session) { 41 | this.session = null; 42 | return { 43 | failed: true, 44 | restart: this.restartOnAuthFail, 45 | failureEventPayload: 'Unable to log in. Are the session details valid?' 46 | }; 47 | } 48 | 49 | return { failed: false }; 50 | } 51 | 52 | async getAuthEventPayload() { 53 | const isMD = await this.client.mPage.evaluate(() => { 54 | return window.Store.MDBackend; 55 | }); 56 | 57 | if(isMD) throw new Error('Authenticating via JSON session is not supported for MultiDevice-enabled WhatsApp accounts.'); 58 | 59 | const localStorage = JSON.parse(await this.client.mPage.evaluate(() => { 60 | return JSON.stringify(window.localStorage); 61 | })); 62 | 63 | return { 64 | WABrowserId: localStorage.WABrowserId, 65 | WASecretBundle: localStorage.WASecretBundle, 66 | WAToken1: localStorage.WAToken1, 67 | WAToken2: localStorage.WAToken2 68 | }; 69 | } 70 | } 71 | 72 | module.exports = LegacySessionAuth; 73 | -------------------------------------------------------------------------------- /src/authStrategies/LinkingMethod.js: -------------------------------------------------------------------------------- 1 | class LinkingMethod { 2 | /** 3 | * 4 | * @typedef QR 5 | * @type {object} 6 | * @property {number} maxRetries - The maximum number of retries to get the QR code before disconnecting 7 | * 8 | * @typedef Phone 9 | * @type {object} 10 | * @property {string} number - The phone number to link with. This should be in the format of (e.g. 5521998765432) 11 | * 12 | * @typedef LinkingMethodData 13 | * @type {object} 14 | * @property {QR} qr - Configuration for QR code linking 15 | * @property {Phone} phone - Configuration for phone number linking 16 | * 17 | * @param {LinkingMethodData} data - Linking method configuration 18 | */ 19 | constructor(data) { 20 | 21 | if (data) this._patch(data); 22 | } 23 | 24 | /** 25 | * 26 | * @param {LinkingMethodData} data 27 | */ 28 | _patch({ phone, qr }) { 29 | if (qr && phone) 30 | throw new Error( 31 | 'Cannot create a link with both QR and phone. Please check the linkingMethod property of the client options.' 32 | ); 33 | this.qr = qr; 34 | this.phone = phone; 35 | } 36 | 37 | isPhone() { 38 | return !!this.phone; 39 | } 40 | 41 | isQR() { 42 | return !!this.qr; 43 | } 44 | } 45 | 46 | module.exports = LinkingMethod; -------------------------------------------------------------------------------- /src/authStrategies/LocalAuth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const BaseAuthStrategy = require('./BaseAuthStrategy'); 6 | 7 | /** 8 | * Local directory-based authentication 9 | * @param {object} options - options 10 | * @param {string} options.clientId - Client id to distinguish instances if you are using multiple, otherwise keep null if you are using only one instance 11 | * @param {string} options.dataPath - Change the default path for saving session files, default is: "./.wwebjs_auth/" 12 | */ 13 | class LocalAuth extends BaseAuthStrategy { 14 | constructor({ clientId, dataPath }={}) { 15 | super(); 16 | 17 | const idRegex = /^[-_\w]+$/i; 18 | if(clientId && !idRegex.test(clientId)) { 19 | throw new Error('Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.'); 20 | } 21 | 22 | this.dataPath = path.resolve(dataPath || './.mywa_auth/'); 23 | this.clientId = clientId; 24 | } 25 | 26 | async beforeBrowserInitialized() { 27 | const playwrightOpts = this.client.options.playwright; 28 | const sessionDirName = this.clientId ? `session-${this.clientId}` : 'session'; 29 | const dirPath = path.join(this.dataPath, sessionDirName); 30 | 31 | if(playwrightOpts.userDataDir && playwrightOpts.userDataDir !== dirPath) { 32 | throw new Error('LocalAuth is not compatible with a user-supplied userDataDir.'); 33 | } 34 | 35 | fs.mkdirSync(dirPath, { recursive: true }); 36 | 37 | this.client.options.playwright = { 38 | ...playwrightOpts, 39 | userDataDir: dirPath 40 | }; 41 | 42 | this.userDataDir = dirPath; 43 | } 44 | 45 | async logout() { 46 | if (this.userDataDir) { 47 | return (fs.rmSync ? fs.rmSync : fs.rmdirSync).call(this, this.userDataDir, { recursive: true, force: true }); 48 | } 49 | } 50 | 51 | } 52 | 53 | module.exports = LocalAuth; 54 | -------------------------------------------------------------------------------- /src/authStrategies/NoAuth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BaseAuthStrategy = require('./BaseAuthStrategy'); 4 | 5 | /** 6 | * No session restoring functionality 7 | * Will need to authenticate via QR code every time 8 | */ 9 | class NoAuth extends BaseAuthStrategy { } 10 | 11 | 12 | module.exports = NoAuth; -------------------------------------------------------------------------------- /src/authStrategies/RemoteAuth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Require Optional Dependencies */ 4 | try { 5 | var fs = require('fs-extra'); 6 | var unzipper = require('unzipper'); 7 | var archiver = require('archiver'); 8 | } catch { 9 | fs = undefined; 10 | unzipper = undefined; 11 | archiver = undefined; 12 | } 13 | 14 | const path = require('path'); 15 | const { Events } = require('./../util/Constants'); 16 | const BaseAuthStrategy = require('./BaseAuthStrategy'); 17 | 18 | /** 19 | * Remote-based authentication 20 | * @param {object} options - options 21 | * @param {object} options.store - Remote database store instance 22 | * @param {string} options.clientId - Client id to distinguish instances if you are using multiple, otherwise keep null if you are using only one instance 23 | * @param {string} options.dataPath - Change the default path for saving session files, default is: "./.wwebjs_auth/" 24 | * @param {number} options.backupSyncIntervalMs - Sets the time interval for periodic session backups. Accepts values starting from 60000ms {1 minute} 25 | */ 26 | class RemoteAuth extends BaseAuthStrategy { 27 | constructor({ clientId, dataPath, store, backupSyncIntervalMs } = {}) { 28 | 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'); 29 | super(); 30 | 31 | const idRegex = /^[-_\w]+$/i; 32 | if (clientId && !idRegex.test(clientId)) { 33 | throw new Error('Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.'); 34 | } 35 | if (!backupSyncIntervalMs || backupSyncIntervalMs < 60000) { 36 | throw new Error('Invalid backupSyncIntervalMs. Accepts values starting from 60000ms {1 minute}.'); 37 | } 38 | if(!store) throw new Error('Remote database store is required.'); 39 | 40 | this.store = store; 41 | this.clientId = clientId; 42 | this.backupSyncIntervalMs = backupSyncIntervalMs; 43 | this.dataPath = path.resolve(dataPath || './.wwebjs_auth/'); 44 | this.tempDir = `${this.dataPath}/wwebjs_temp_session_${this.clientId}`; 45 | this.requiredDirs = ['Default', 'IndexedDB', 'Local Storage']; /* => Required Files & Dirs in WWebJS to restore session */ 46 | } 47 | 48 | async beforeBrowserInitialized() { 49 | const playwrightOpts = this.client.options.playwright; 50 | const sessionDirName = this.clientId ? `RemoteAuth-${this.clientId}` : 'RemoteAuth'; 51 | const dirPath = path.join(this.dataPath, sessionDirName); 52 | 53 | if (playwrightOpts.userDataDir && playwrightOpts.userDataDir !== dirPath) { 54 | throw new Error('RemoteAuth is not compatible with a user-supplied userDataDir.'); 55 | } 56 | 57 | this.userDataDir = dirPath; 58 | this.sessionName = sessionDirName; 59 | 60 | await this.extractRemoteSession(); 61 | 62 | this.client.options.playwright = { 63 | ...playwrightOpts, 64 | userDataDir: dirPath 65 | }; 66 | } 67 | 68 | async logout() { 69 | await this.disconnect(); 70 | } 71 | 72 | async destroy() { 73 | clearInterval(this.backupSync); 74 | } 75 | 76 | async disconnect() { 77 | await this.deleteRemoteSession(); 78 | 79 | let pathExists = await this.isValidPath(this.userDataDir); 80 | if (pathExists) { 81 | await fs.promises.rm(this.userDataDir, { 82 | recursive: true, 83 | force: true 84 | }).catch(() => {}); 85 | } 86 | clearInterval(this.backupSync); 87 | } 88 | 89 | async afterAuthReady() { 90 | const sessionExists = await this.store.sessionExists({session: this.sessionName}); 91 | if(!sessionExists) { 92 | await this.delay(60000); /* Initial delay sync required for session to be stable enough to recover */ 93 | await this.storeRemoteSession({emit: true}); 94 | } 95 | var self = this; 96 | this.backupSync = setInterval(async function () { 97 | await self.storeRemoteSession(); 98 | }, this.backupSyncIntervalMs); 99 | } 100 | 101 | async storeRemoteSession(options) { 102 | /* Compress & Store Session */ 103 | const pathExists = await this.isValidPath(this.userDataDir); 104 | if (pathExists) { 105 | await this.compressSession(); 106 | await this.store.save({session: this.sessionName}); 107 | await fs.promises.unlink(`${this.sessionName}.zip`); 108 | await fs.promises.rm(`${this.tempDir}`, { 109 | recursive: true, 110 | force: true 111 | }).catch(() => {}); 112 | if(options && options.emit) this.client.emit(Events.REMOTE_SESSION_SAVED); 113 | } 114 | } 115 | 116 | async extractRemoteSession() { 117 | const pathExists = await this.isValidPath(this.userDataDir); 118 | const compressedSessionPath = `${this.sessionName}.zip`; 119 | const sessionExists = await this.store.sessionExists({session: this.sessionName}); 120 | if (pathExists) { 121 | await fs.promises.rm(this.userDataDir, { 122 | recursive: true, 123 | force: true 124 | }).catch(() => {}); 125 | } 126 | if (sessionExists) { 127 | await this.store.extract({session: this.sessionName, path: compressedSessionPath}); 128 | await this.unCompressSession(compressedSessionPath); 129 | } else { 130 | fs.mkdirSync(this.userDataDir, { recursive: true }); 131 | } 132 | } 133 | 134 | async deleteRemoteSession() { 135 | const sessionExists = await this.store.sessionExists({session: this.sessionName}); 136 | if (sessionExists) await this.store.delete({session: this.sessionName}); 137 | } 138 | 139 | async compressSession() { 140 | const archive = archiver('zip'); 141 | const stream = fs.createWriteStream(`${this.sessionName}.zip`); 142 | 143 | await fs.copy(this.userDataDir, this.tempDir).catch(() => {}); 144 | await this.deleteMetadata(); 145 | return new Promise((resolve, reject) => { 146 | archive 147 | .directory(this.tempDir, false) 148 | .on('error', err => reject(err)) 149 | .pipe(stream); 150 | 151 | stream.on('close', () => resolve()); 152 | archive.finalize(); 153 | }); 154 | } 155 | 156 | async unCompressSession(compressedSessionPath) { 157 | var stream = fs.createReadStream(compressedSessionPath); 158 | await new Promise((resolve, reject) => { 159 | stream.pipe(unzipper.Extract({ 160 | path: this.userDataDir 161 | })) 162 | .on('error', err => reject(err)) 163 | .on('finish', () => resolve()); 164 | }); 165 | await fs.promises.unlink(compressedSessionPath); 166 | } 167 | 168 | async deleteMetadata() { 169 | const sessionDirs = [this.tempDir, path.join(this.tempDir, 'Default')]; 170 | for (const dir of sessionDirs) { 171 | const sessionFiles = await fs.promises.readdir(dir); 172 | for (const element of sessionFiles) { 173 | if (!this.requiredDirs.includes(element)) { 174 | const dirElement = path.join(dir, element); 175 | const stats = await fs.promises.lstat(dirElement); 176 | 177 | if (stats.isDirectory()) { 178 | await fs.promises.rm(dirElement, { 179 | recursive: true, 180 | force: true 181 | }).catch(() => {}); 182 | } else { 183 | await fs.promises.unlink(dirElement).catch(() => {}); 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | async isValidPath(path) { 191 | try { 192 | await fs.promises.access(path); 193 | return true; 194 | } catch { 195 | return false; 196 | } 197 | } 198 | 199 | async delay(ms) { 200 | return new Promise(resolve => setTimeout(resolve, ms)); 201 | } 202 | } 203 | 204 | module.exports = RemoteAuth; 205 | -------------------------------------------------------------------------------- /src/factories/ChatFactory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PrivateChat = require('../structures/PrivateChat'); 4 | const GroupChat = require('../structures/GroupChat'); 5 | 6 | class ChatFactory { 7 | static create(client, data) { 8 | if(data.isGroup) { 9 | return new GroupChat(client, data); 10 | } 11 | 12 | return new PrivateChat(client, data); 13 | } 14 | } 15 | 16 | module.exports = ChatFactory; -------------------------------------------------------------------------------- /src/factories/ContactFactory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PrivateContact = require('../structures/PrivateContact'); 4 | const BusinessContact = require('../structures/BusinessContact'); 5 | 6 | class ContactFactory { 7 | static create(client, data) { 8 | if(data.isBusiness) { 9 | return new BusinessContact(client, data); 10 | } 11 | 12 | return new PrivateContact(client, data); 13 | } 14 | } 15 | 16 | module.exports = ContactFactory; -------------------------------------------------------------------------------- /src/structures/Base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Represents a WhatsApp data structure 5 | */ 6 | class Base { 7 | constructor(client) { 8 | /** 9 | * The client that instantiated this 10 | * @readonly 11 | */ 12 | Object.defineProperty(this, 'client', { value: client }); 13 | } 14 | 15 | _clone() { 16 | return Object.assign(Object.create(this), this); 17 | } 18 | 19 | _patch(data) { return data; } 20 | } 21 | 22 | module.exports = Base; -------------------------------------------------------------------------------- /src/structures/BusinessContact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Contact = require('./Contact'); 4 | 5 | /** 6 | * Represents a Business Contact on WhatsApp 7 | * @extends {Contact} 8 | */ 9 | class BusinessContact extends Contact { 10 | _patch(data) { 11 | /** 12 | * The contact's business profile 13 | */ 14 | this.businessProfile = data.businessProfile; 15 | 16 | return super._patch(data); 17 | } 18 | 19 | } 20 | 21 | module.exports = BusinessContact; -------------------------------------------------------------------------------- /src/structures/Buttons.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MessageMedia = require('./MessageMedia'); 4 | const Util = require('../util/Util'); 5 | 6 | /** 7 | * Button spec used in Buttons constructor 8 | * @typedef {Object} ButtonSpec 9 | * @property {string=} id - Custom ID to set on the button. A random one will be generated if one is not passed. 10 | * @property {string} body - The text to show on the button. 11 | */ 12 | 13 | /** 14 | * @typedef {Object} FormattedButtonSpec 15 | * @property {string} buttonId 16 | * @property {number} type 17 | * @property {Object} buttonText 18 | */ 19 | 20 | /** 21 | * Message type buttons 22 | */ 23 | class Buttons { 24 | /** 25 | * @param {string|MessageMedia} body 26 | * @param {ButtonSpec[]} buttons - See {@link ButtonSpec} 27 | * @param {string?} title 28 | * @param {string?} footer 29 | */ 30 | constructor(body, buttons, title, footer) { 31 | /** 32 | * Message body 33 | * @type {string|MessageMedia} 34 | */ 35 | this.body = body; 36 | 37 | /** 38 | * title of message 39 | * @type {string} 40 | */ 41 | this.title = title; 42 | 43 | /** 44 | * footer of message 45 | * @type {string} 46 | */ 47 | this.footer = footer; 48 | 49 | if (body instanceof MessageMedia) { 50 | this.type = 'media'; 51 | this.title = ''; 52 | }else{ 53 | this.type = 'chat'; 54 | } 55 | 56 | /** 57 | * buttons of message 58 | * @type {FormattedButtonSpec[]} 59 | */ 60 | this.buttons = this._format(buttons); 61 | if(!this.buttons.length){ throw '[BT01] No buttons';} 62 | 63 | } 64 | 65 | /** 66 | * Creates button array from simple array 67 | * @param {ButtonSpec[]} buttons 68 | * @returns {FormattedButtonSpec[]} 69 | * @example 70 | * Input: [{id:'customId',body:'button1'},{body:'button2'},{body:'button3'},{body:'button4'}] 71 | * Returns: [{ buttonId:'customId',buttonText:{'displayText':'button1'},type: 1 },{buttonId:'n3XKsL',buttonText:{'displayText':'button2'},type:1},{buttonId:'NDJk0a',buttonText:{'displayText':'button3'},type:1}] 72 | */ 73 | _format(buttons){ 74 | buttons = buttons.slice(0,3); // phone users can only see 3 buttons, so lets limit this 75 | return buttons.map((btn) => { 76 | return {'buttonId':btn.id ? String(btn.id) : Util.generateHash(6),'buttonText':{'displayText':btn.body},'type':1}; 77 | }); 78 | } 79 | 80 | } 81 | 82 | module.exports = Buttons; -------------------------------------------------------------------------------- /src/structures/Call.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | 5 | /** 6 | * Represents a Call on WhatsApp 7 | * @extends {Base} 8 | */ 9 | class Call extends Base { 10 | constructor(client, data) { 11 | super(client); 12 | 13 | if (data) this._patch(data); 14 | } 15 | 16 | _patch(data) { 17 | /** 18 | * Call ID 19 | * @type {string} 20 | */ 21 | this.id = data.id; 22 | /** 23 | * From 24 | * @type {string} 25 | */ 26 | this.from = data.peerJid; 27 | /** 28 | * Unix timestamp for when the call was created 29 | * @type {number} 30 | */ 31 | this.timestamp = data.offerTime; 32 | /** 33 | * Is video 34 | * @type {boolean} 35 | */ 36 | this.isVideo = data.isVideo; 37 | /** 38 | * Is Group 39 | * @type {boolean} 40 | */ 41 | this.isGroup = data.isGroup; 42 | /** 43 | * Indicates if the call was sent by the current user 44 | * @type {boolean} 45 | */ 46 | this.fromMe = data.outgoing; 47 | /** 48 | * Indicates if the call can be handled in waweb 49 | * @type {boolean} 50 | */ 51 | this.canHandleLocally = data.canHandleLocally; 52 | /** 53 | * Indicates if the call Should be handled in waweb 54 | * @type {boolean} 55 | */ 56 | this.webClientShouldHandle = data.webClientShouldHandle; 57 | /** 58 | * Object with participants 59 | * @type {object} 60 | */ 61 | this.participants = data.participants; 62 | 63 | return super._patch(data); 64 | } 65 | 66 | /** 67 | * Reject the call 68 | */ 69 | async reject() { 70 | return this.client.mPage.evaluate( 71 | ({ peerJid, id }) => { 72 | return window.WWebJS.rejectCall(peerJid, id); 73 | }, 74 | { peerJid: this.from, id: this.id } 75 | ); 76 | } 77 | } 78 | 79 | module.exports = Call; 80 | -------------------------------------------------------------------------------- /src/structures/Chat.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | const Message = require("./Message"); 5 | 6 | /** 7 | * Represents a Chat on WhatsApp 8 | * @extends {Base} 9 | */ 10 | class Chat extends Base { 11 | constructor(client, data) { 12 | super(client); 13 | 14 | if (data) this._patch(data); 15 | } 16 | 17 | _patch(data) { 18 | /** 19 | * ID that represents the chat 20 | * @type {object} 21 | */ 22 | this.id = data.id; 23 | 24 | /** 25 | * Title of the chat 26 | * @type {string} 27 | */ 28 | this.name = data.formattedTitle; 29 | 30 | /** 31 | * Indicates if the Chat is a Group Chat 32 | * @type {boolean} 33 | */ 34 | this.isGroup = data.isGroup; 35 | 36 | /** 37 | * Indicates if the Chat is readonly 38 | * @type {boolean} 39 | */ 40 | this.isReadOnly = data.isReadOnly; 41 | 42 | /** 43 | * Amount of messages unread 44 | * @type {number} 45 | */ 46 | this.unreadCount = data.unreadCount; 47 | 48 | /** 49 | * Unix timestamp for when the last activity occurred 50 | * @type {number} 51 | */ 52 | this.timestamp = data.t; 53 | 54 | /** 55 | * Indicates if the Chat is archived 56 | * @type {boolean} 57 | */ 58 | this.archived = data.archive; 59 | 60 | /** 61 | * Indicates if the Chat is pinned 62 | * @type {boolean} 63 | */ 64 | this.pinned = !!data.pin; 65 | 66 | /** 67 | * Indicates if the chat is muted or not 68 | * @type {boolean} 69 | */ 70 | this.isMuted = data.isMuted; 71 | 72 | /** 73 | * Unix timestamp for when the mute expires 74 | * @type {number} 75 | */ 76 | this.muteExpiration = data.muteExpiration; 77 | 78 | /** 79 | * Last message fo chat 80 | * @type {Message} 81 | */ 82 | this.lastMessage = data.lastMessage 83 | ? new Message(super.client, data.lastMessage) 84 | : undefined; 85 | 86 | return super._patch(data); 87 | } 88 | 89 | /** 90 | * Send a message to this chat 91 | * @param {string|MessageMedia|Location} content 92 | * @param {MessageSendOptions} [options] 93 | * @returns {Promise} Message that was just sent 94 | */ 95 | async sendMessage(content, options) { 96 | return this.client.sendMessage(this.id._serialized, content, options); 97 | } 98 | 99 | /** 100 | * Set the message as seen 101 | * @returns {Promise} result 102 | */ 103 | async sendSeen() { 104 | return this.client.sendSeen(this.id._serialized); 105 | } 106 | 107 | /** 108 | * Clears all messages from the chat 109 | * @returns {Promise} result 110 | */ 111 | async clearMessages() { 112 | return this.client.mPage.evaluate((chatId) => { 113 | return window.WWebJS.sendClearChat(chatId); 114 | }, this.id._serialized); 115 | } 116 | 117 | /** 118 | * Deletes the chat 119 | * @returns {Promise} result 120 | */ 121 | async delete() { 122 | return this.client.mPage.evaluate((chatId) => { 123 | return window.WWebJS.sendDeleteChat(chatId); 124 | }, this.id._serialized); 125 | } 126 | 127 | /** 128 | * Archives this chat 129 | */ 130 | async archive() { 131 | return this.client.archiveChat(this.id._serialized); 132 | } 133 | 134 | /** 135 | * un-archives this chat 136 | */ 137 | async unarchive() { 138 | return this.client.unarchiveChat(this.id._serialized); 139 | } 140 | 141 | /** 142 | * Pins this chat 143 | * @returns {Promise} New pin state. Could be false if the max number of pinned chats was reached. 144 | */ 145 | async pin() { 146 | return this.client.pinChat(this.id._serialized); 147 | } 148 | 149 | /** 150 | * Unpins this chat 151 | * @returns {Promise} New pin state 152 | */ 153 | async unpin() { 154 | return this.client.unpinChat(this.id._serialized); 155 | } 156 | 157 | /** 158 | * Mutes this chat forever, unless a date is specified 159 | * @param {?Date} unmuteDate Date at which the Chat will be unmuted, leave as is to mute forever 160 | */ 161 | async mute(unmuteDate) { 162 | return this.client.muteChat(this.id._serialized, unmuteDate); 163 | } 164 | 165 | /** 166 | * Unmutes this chat 167 | */ 168 | async unmute() { 169 | return this.client.unmuteChat(this.id._serialized); 170 | } 171 | 172 | /** 173 | * Mark this chat as unread 174 | */ 175 | async markUnread() { 176 | return this.client.markChatUnread(this.id._serialized); 177 | } 178 | 179 | /** 180 | * Loads chat messages, sorted from earliest to latest. 181 | * @param {Object} searchOptions Options for searching messages. Right now only limit and fromMe is supported. 182 | * @param {Number} [searchOptions.limit] The amount of messages to return. If no limit is specified, the available messages will be returned. Note that the actual number of returned messages may be smaller if there aren't enough messages in the conversation. Set this to Infinity to load all messages. 183 | * @param {Boolean} [searchOptions.fromMe] Return only messages from the bot number or vise versa. To get all messages, leave the option undefined. 184 | * @returns {Promise>} 185 | */ 186 | async fetchMessages(searchOptions) { 187 | let messages = await this.client.mPage.evaluate( 188 | async ({ chatId, searchOptions }) => { 189 | const msgFilter = (m) => { 190 | if (m.isNotification) { 191 | return false; // dont include notification messages 192 | } 193 | if ( 194 | searchOptions && 195 | searchOptions.fromMe !== undefined && 196 | m.id.fromMe !== searchOptions.fromMe 197 | ) { 198 | return false; 199 | } 200 | return true; 201 | }; 202 | 203 | const chat = window.Store.Chat.get(chatId); 204 | let msgs = chat.msgs.getModelsArray().filter(msgFilter); 205 | 206 | if (searchOptions && searchOptions.limit > 0) { 207 | while (msgs.length < searchOptions.limit) { 208 | const loadedMessages = 209 | await window.Store.ConversationMsgs.loadEarlierMsgs(chat); 210 | if (!loadedMessages || !loadedMessages.length) break; 211 | msgs = [...loadedMessages.filter(msgFilter), ...msgs]; 212 | } 213 | 214 | if (msgs.length > searchOptions.limit) { 215 | msgs.sort((a, b) => (a.t > b.t ? 1 : -1)); 216 | msgs = msgs.splice(msgs.length - searchOptions.limit); 217 | } 218 | } 219 | 220 | return msgs.map((m) => window.WWebJS.getMessageModel(m)); 221 | }, 222 | { chatId: this.id._serialized, searchOptions } 223 | ); 224 | 225 | return messages.map((m) => new Message(this.client, m)); 226 | } 227 | 228 | /** 229 | * Simulate typing in chat. This will last for 25 seconds. 230 | */ 231 | async sendStateTyping() { 232 | return this.client.mPage.evaluate((chatId) => { 233 | window.WWebJS.sendChatstate("typing", chatId); 234 | return true; 235 | }, this.id._serialized); 236 | } 237 | 238 | /** 239 | * Simulate recording audio in chat. This will last for 25 seconds. 240 | */ 241 | async sendStateRecording() { 242 | return this.client.mPage.evaluate((chatId) => { 243 | window.WWebJS.sendChatstate("recording", chatId); 244 | return true; 245 | }, this.id._serialized); 246 | } 247 | 248 | /** 249 | * Stops typing or recording in chat immediately. 250 | */ 251 | async clearState() { 252 | return this.client.mPage.evaluate((chatId) => { 253 | window.WWebJS.sendChatstate("stop", chatId); 254 | return true; 255 | }, this.id._serialized); 256 | } 257 | 258 | /** 259 | * Returns the Contact that corresponds to this Chat. 260 | * @returns {Promise} 261 | */ 262 | async getContact() { 263 | return await this.client.getContactById(this.id._serialized); 264 | } 265 | 266 | /** 267 | * Returns array of all Labels assigned to this Chat 268 | * @returns {Promise>} 269 | */ 270 | async getLabels() { 271 | return this.client.getChatLabels(this.id._serialized); 272 | } 273 | 274 | /** 275 | * Add or remove labels to this Chat 276 | * @param {Array} labelIds 277 | * @returns {Promise} 278 | */ 279 | async changeLabels(labelIds) { 280 | return this.client.addOrRemoveLabels(labelIds, [this.id._serialized]); 281 | } 282 | } 283 | 284 | module.exports = Chat; 285 | -------------------------------------------------------------------------------- /src/structures/ClientInfo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./Base'); 4 | 5 | /** 6 | * Current connection information 7 | * @extends {Base} 8 | */ 9 | class ClientInfo extends Base { 10 | constructor(client, data) { 11 | super(client); 12 | 13 | if (data) this._patch(data); 14 | } 15 | 16 | _patch(data) { 17 | /** 18 | * Name configured to be shown in push notifications 19 | * @type {string} 20 | */ 21 | this.pushname = data.pushname; 22 | 23 | /** 24 | * Current user ID 25 | * @type {object} 26 | */ 27 | this.wid = data.wid; 28 | 29 | /** 30 | * @type {object} 31 | * @deprecated Use .wid instead 32 | */ 33 | this.me = data.wid; 34 | 35 | /** 36 | * Information about the phone this client is connected to. Not available in multi-device. 37 | * @type {object} 38 | * @property {string} wa_version WhatsApp Version running on the phone 39 | * @property {string} os_version OS Version running on the phone (iOS or Android version) 40 | * @property {string} device_manufacturer Device manufacturer 41 | * @property {string} device_model Device model 42 | * @property {string} os_build_number OS build number 43 | * @deprecated 44 | */ 45 | this.phone = data.phone; 46 | 47 | /** 48 | * Platform WhatsApp is running on 49 | * @type {string} 50 | */ 51 | this.platform = data.platform; 52 | 53 | return super._patch(data); 54 | } 55 | 56 | /** 57 | * Get current battery percentage and charging status for the attached device 58 | * @returns {object} batteryStatus 59 | * @returns {number} batteryStatus.battery - The current battery percentage 60 | * @returns {boolean} batteryStatus.plugged - Indicates if the phone is plugged in (true) or not (false) 61 | * @deprecated 62 | */ 63 | async getBatteryStatus() { 64 | return await this.client.mPage.evaluate(() => { 65 | const { battery, plugged } = window.Store.Conn; 66 | return { battery, plugged }; 67 | }); 68 | } 69 | } 70 | 71 | module.exports = ClientInfo; -------------------------------------------------------------------------------- /src/structures/Contact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./Base'); 4 | 5 | /** 6 | * ID that represents a contact 7 | * @typedef {Object} ContactId 8 | * @property {string} server 9 | * @property {string} user 10 | * @property {string} _serialized 11 | */ 12 | 13 | /** 14 | * Represents a Contact on WhatsApp 15 | * @extends {Base} 16 | */ 17 | class Contact extends Base { 18 | constructor(client, data) { 19 | super(client); 20 | 21 | if(data) this._patch(data); 22 | } 23 | 24 | _patch(data) { 25 | /** 26 | * ID that represents the contact 27 | * @type {ContactId} 28 | */ 29 | this.id = data.id; 30 | 31 | /** 32 | * Contact's phone number 33 | * @type {string} 34 | */ 35 | this.number = data.userid; 36 | 37 | /** 38 | * Indicates if the contact is a business contact 39 | * @type {boolean} 40 | */ 41 | this.isBusiness = data.isBusiness; 42 | 43 | /** 44 | * Indicates if the contact is an enterprise contact 45 | * @type {boolean} 46 | */ 47 | this.isEnterprise = data.isEnterprise; 48 | 49 | this.labels = data.labels; 50 | 51 | /** 52 | * The contact's name, as saved by the current user 53 | * @type {?string} 54 | */ 55 | this.name = data.name; 56 | 57 | /** 58 | * The name that the contact has configured to be shown publically 59 | * @type {string} 60 | */ 61 | this.pushname = data.pushname; 62 | 63 | this.sectionHeader = data.sectionHeader; 64 | 65 | /** 66 | * A shortened version of name 67 | * @type {?string} 68 | */ 69 | this.shortName = data.shortName; 70 | 71 | this.statusMute = data.statusMute; 72 | this.type = data.type; 73 | this.verifiedLevel = data.verifiedLevel; 74 | this.verifiedName = data.verifiedName; 75 | 76 | /** 77 | * Indicates if the contact is the current user's contact 78 | * @type {boolean} 79 | */ 80 | this.isMe = data.isMe; 81 | 82 | /** 83 | * Indicates if the contact is a user contact 84 | * @type {boolean} 85 | */ 86 | this.isUser = data.isUser; 87 | 88 | /** 89 | * Indicates if the contact is a group contact 90 | * @type {boolean} 91 | */ 92 | this.isGroup = data.isGroup; 93 | 94 | /** 95 | * Indicates if the number is registered on WhatsApp 96 | * @type {boolean} 97 | */ 98 | this.isWAContact = data.isWAContact; 99 | 100 | /** 101 | * Indicates if the number is saved in the current phone's contacts 102 | * @type {boolean} 103 | */ 104 | this.isMyContact = data.isMyContact; 105 | 106 | /** 107 | * Indicates if you have blocked this contact 108 | * @type {boolean} 109 | */ 110 | this.isBlocked = data.isBlocked; 111 | 112 | return super._patch(data); 113 | } 114 | 115 | /** 116 | * Returns the contact's profile picture URL, if privacy settings allow it 117 | * @returns {Promise} 118 | */ 119 | async getProfilePicUrl() { 120 | return await this.client.getProfilePicUrl(this.id._serialized); 121 | } 122 | 123 | /** 124 | * Returns the contact's formatted phone number, (12345678901@c.us) => (+1 (234) 5678-901) 125 | * @returns {Promise} 126 | */ 127 | async getFormattedNumber() { 128 | return await this.client.getFormattedNumber(this.id._serialized); 129 | } 130 | 131 | /** 132 | * Returns the contact's countrycode, (1541859685@c.us) => (1) 133 | * @returns {Promise} 134 | */ 135 | async getCountryCode() { 136 | return await this.client.getCountryCode(this.id._serialized); 137 | } 138 | 139 | /** 140 | * Returns the Chat that corresponds to this Contact. 141 | * Will return null when getting chat for currently logged in user. 142 | * @returns {Promise} 143 | */ 144 | async getChat() { 145 | if(this.isMe) return null; 146 | 147 | return await this.client.getChatById(this.id._serialized); 148 | } 149 | 150 | /** 151 | * Blocks this contact from WhatsApp 152 | * @returns {Promise} 153 | */ 154 | async block() { 155 | if(this.isGroup) return false; 156 | 157 | await this.client.mPage.evaluate(async (contactId) => { 158 | const contact = window.Store.Contact.get(contactId); 159 | await window.Store.BlockContact.blockContact({contact}); 160 | }, this.id._serialized); 161 | 162 | this.isBlocked = true; 163 | return true; 164 | } 165 | 166 | /** 167 | * Unblocks this contact from WhatsApp 168 | * @returns {Promise} 169 | */ 170 | async unblock() { 171 | if(this.isGroup) return false; 172 | 173 | await this.client.mPage.evaluate(async (contactId) => { 174 | const contact = window.Store.Contact.get(contactId); 175 | await window.Store.BlockContact.unblockContact(contact); 176 | }, this.id._serialized); 177 | 178 | this.isBlocked = false; 179 | return true; 180 | } 181 | 182 | /** 183 | * Gets the Contact's current "about" info. Returns null if you don't have permission to read their status. 184 | * @returns {Promise} 185 | */ 186 | async getAbout() { 187 | const about = await this.client.mPage.evaluate(async (contactId) => { 188 | const wid = window.Store.WidFactory.createWid(contactId); 189 | return window.Store.StatusUtils.getStatus(wid); 190 | }, this.id._serialized); 191 | 192 | if (typeof about.status !== 'string') 193 | return null; 194 | 195 | return about.status; 196 | } 197 | 198 | /** 199 | * Gets the Contact's common groups with you. Returns empty array if you don't have any common group. 200 | * @returns {Promise} 201 | */ 202 | async getCommonGroups() { 203 | return await this.client.getCommonGroups(this.id._serialized); 204 | } 205 | 206 | } 207 | 208 | module.exports = Contact; 209 | -------------------------------------------------------------------------------- /src/structures/GroupChat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Chat = require('./Chat'); 4 | 5 | /** 6 | * Group participant information 7 | * @typedef {Object} GroupParticipant 8 | * @property {ContactId} id 9 | * @property {boolean} isAdmin 10 | * @property {boolean} isSuperAdmin 11 | */ 12 | 13 | /** 14 | * Represents a Group Chat on WhatsApp 15 | * @extends {Chat} 16 | */ 17 | class GroupChat extends Chat { 18 | _patch(data) { 19 | this.groupMetadata = data.groupMetadata; 20 | 21 | return super._patch(data); 22 | } 23 | 24 | /** 25 | * Gets the group owner 26 | * @type {ContactId} 27 | */ 28 | get owner() { 29 | return this.groupMetadata.owner; 30 | } 31 | 32 | /** 33 | * Gets the date at which the group was created 34 | * @type {date} 35 | */ 36 | get createdAt() { 37 | return new Date(this.groupMetadata.creation * 1000); 38 | } 39 | 40 | /** 41 | * Gets the group description 42 | * @type {string} 43 | */ 44 | get description() { 45 | return this.groupMetadata.desc; 46 | } 47 | 48 | /** 49 | * Gets the group participants 50 | * @type {Array} 51 | */ 52 | get participants() { 53 | return this.groupMetadata.participants; 54 | } 55 | 56 | /** 57 | * An object that handles the result for {@link addParticipants} method 58 | * @typedef {Object} AddParticipantsResult 59 | * @property {number} code The code of the result 60 | * @property {string} message The result message 61 | * @property {boolean} isInviteV4Sent Indicates if the inviteV4 was sent to the partitipant 62 | */ 63 | 64 | /** 65 | * An object that handles options for adding participants 66 | * @typedef {Object} AddParticipnatsOptions 67 | * @property {Array|number} [sleep = [250, 500]] The number of milliseconds to wait before adding the next participant. If it is an array, a random sleep time between the sleep[0] and sleep[1] values will be added (the difference must be >=100 ms, otherwise, a random sleep time between sleep[1] and sleep[1] + 100 will be added). If sleep is a number, a sleep time equal to its value will be added. By default, sleep is an array with a value of [250, 500] 68 | * @property {boolean} [autoSendInviteV4 = true] If true, the inviteV4 will be sent to those participants who have restricted others from being automatically added to groups, otherwise the inviteV4 won't be sent (true by default) 69 | * @property {string} [comment = ''] The comment to be added to an inviteV4 (empty string by default) 70 | */ 71 | 72 | /** 73 | * Adds a list of participants by ID to the group 74 | * @param {string|Array} participantIds 75 | * @param {AddParticipnatsOptions} options An object thay handles options for adding participants 76 | * @returns {Promise|string>} Returns an object with the resulting data or an error message as a string 77 | */ 78 | async addParticipants(participantIds, options = {}) { 79 | return await this.client.mPage.evaluate(async ({groupId, participantIds, options}) => { 80 | const { sleep = [250, 500], autoSendInviteV4 = true, comment = '' } = options; 81 | const participantData = {}; 82 | 83 | !Array.isArray(participantIds) && (participantIds = [participantIds]); 84 | const groupWid = window.Store.WidFactory.createWid(groupId); 85 | const group = await window.Store.Chat.find(groupWid); 86 | const participantWids = participantIds.map((p) => window.Store.WidFactory.createWid(p)); 87 | 88 | const errorCodes = { 89 | default: 'An unknown error occupied while adding a participant', 90 | isGroupEmpty: 'AddParticipantsError: The participant can\'t be added to an empty group', 91 | iAmNotAdmin: 'AddParticipantsError: You have no admin rights to add a participant to a group', 92 | 200: 'The participant was added successfully', 93 | 403: 'The participant can be added by sending private invitation only', 94 | 404: 'The phone number is not registered on WhatsApp', 95 | 408: 'You cannot add this participant because they recently left the group', 96 | 409: 'The participant is already a group member', 97 | 417: 'The participant can\'t be added to the community. You can invite them privately to join this group through its invite link', 98 | 419: 'The participant can\'t be added because the group is full' 99 | }; 100 | 101 | await window.Store.GroupMetadata.queryAndUpdate(groupWid); 102 | const groupMetadata = group.groupMetadata; 103 | const groupParticipants = groupMetadata?.participants; 104 | 105 | if (!groupParticipants) { 106 | return errorCodes.isGroupEmpty; 107 | } 108 | 109 | if (!group.iAmAdmin()) { 110 | return errorCodes.iAmNotAdmin; 111 | } 112 | 113 | const _getSleepTime = (sleep) => { 114 | if (!Array.isArray(sleep) || sleep.length === 2 && sleep[0] === sleep[1]) { 115 | return sleep; 116 | } 117 | if (sleep.length === 1) { 118 | return sleep[0]; 119 | } 120 | (sleep[1] - sleep[0]) < 100 && (sleep[0] = sleep[1]) && (sleep[1] += 100); 121 | return Math.floor(Math.random() * (sleep[1] - sleep[0] + 1)) + sleep[0]; 122 | }; 123 | 124 | for (const pWid of participantWids) { 125 | const pId = pWid._serialized; 126 | 127 | participantData[pId] = { 128 | code: undefined, 129 | message: undefined, 130 | isInviteV4Sent: false 131 | }; 132 | 133 | if (groupParticipants.some(p => p.id._serialized === pId)) { 134 | participantData[pId].code = 409; 135 | participantData[pId].message = errorCodes[409]; 136 | continue; 137 | } 138 | 139 | if (!(await window.Store.QueryExist(pWid))?.wid) { 140 | participantData[pId].code = 404; 141 | participantData[pId].message = errorCodes[404]; 142 | continue; 143 | } 144 | 145 | const rpcResult = 146 | await window.WWebJS.getAddParticipantsRpcResult(groupMetadata, groupWid, pWid); 147 | const { code: rpcResultCode } = rpcResult; 148 | 149 | participantData[pId].code = rpcResultCode; 150 | participantData[pId].message = 151 | errorCodes[rpcResultCode] || errorCodes.default; 152 | 153 | if (autoSendInviteV4 && rpcResultCode === 403) { 154 | let userChat, isInviteV4Sent = false; 155 | window.Store.ContactCollection.gadd(pWid, { silent: true }); 156 | 157 | if (rpcResult.name === 'ParticipantRequestCodeCanBeSent' && 158 | (userChat = await window.Store.Chat.find(pWid))) { 159 | const groupName = group.formattedTitle || group.name; 160 | const res = await window.Store.GroupInviteV4.sendGroupInviteMessage( 161 | userChat, 162 | group.id._serialized, 163 | groupName, 164 | rpcResult.inviteV4Code, 165 | rpcResult.inviteV4CodeExp, 166 | comment, 167 | await window.WWebJS.getProfilePicThumbToBase64(groupWid) 168 | ); 169 | isInviteV4Sent = window.compareWwebVersions(window.Debug.VERSION, '<', '2.2335.6') 170 | ? res === 'OK' 171 | : res.messageSendResult === 'OK'; 172 | } 173 | 174 | participantData[pId].isInviteV4Sent = isInviteV4Sent; 175 | } 176 | 177 | sleep && 178 | participantWids.length > 1 && 179 | participantWids.indexOf(pWid) !== participantWids.length - 1 && 180 | (await new Promise((resolve) => setTimeout(resolve, _getSleepTime(sleep)))); 181 | } 182 | 183 | return participantData; 184 | }, { groupId: this.id._serialized, participantIds, options}); 185 | } 186 | 187 | /** 188 | * Removes a list of participants by ID to the group 189 | * @param {Array} participantIds 190 | * @returns {Promise<{ status: number }>} 191 | */ 192 | async removeParticipants(participantIds) { 193 | return await this.client.mPage.evaluate(async ({chatId, participantIds}) => { 194 | const chatWid = window.Store.WidFactory.createWid(chatId); 195 | const chat = await window.Store.Chat.find(chatWid); 196 | const participants = participantIds.map(p => { 197 | return chat.groupMetadata.participants.get(p); 198 | }).filter(p => Boolean(p)); 199 | await window.Store.GroupParticipants.removeParticipants(chat, participants); 200 | return { status: 200 }; 201 | }, {chatId: this.id._serialized, participantIds}); 202 | } 203 | 204 | /** 205 | * Promotes participants by IDs to admins 206 | * @param {Array} participantIds 207 | * @returns {Promise<{ status: number }>} Object with status code indicating if the operation was successful 208 | */ 209 | async promoteParticipants(participantIds) { 210 | return await this.client.mPage.evaluate(async ({chatId, participantIds}) => { 211 | const chatWid = window.Store.WidFactory.createWid(chatId); 212 | const chat = await window.Store.Chat.find(chatWid); 213 | const participants = participantIds.map(p => { 214 | return chat.groupMetadata.participants.get(p); 215 | }).filter(p => Boolean(p)); 216 | await window.Store.GroupParticipants.promoteParticipants(chat, participants); 217 | return { status: 200 }; 218 | }, { chatId: this.id._serialized, participantIds}); 219 | } 220 | 221 | /** 222 | * Demotes participants by IDs to regular users 223 | * @param {Array} participantIds 224 | * @returns {Promise<{ status: number }>} Object with status code indicating if the operation was successful 225 | */ 226 | async demoteParticipants(participantIds) { 227 | return await this.client.mPage.evaluate(async ({chatId, participantIds}) => { 228 | const chatWid = window.Store.WidFactory.createWid(chatId); 229 | const chat = await window.Store.Chat.find(chatWid); 230 | const participants = participantIds.map(p => { 231 | return chat.groupMetadata.participants.get(p); 232 | }).filter(p => Boolean(p)); 233 | await window.Store.GroupParticipants.demoteParticipants(chat, participants); 234 | return { status: 200 }; 235 | }, { chatId: this.id._serialized, participantIds}); 236 | } 237 | 238 | /** 239 | * Updates the group subject 240 | * @param {string} subject 241 | * @returns {Promise} Returns true if the subject was properly updated. This can return false if the user does not have the necessary permissions. 242 | */ 243 | async setSubject(subject) { 244 | const success = await this.client.mPage.evaluate(async ({chatId, subject}) => { 245 | const chatWid = window.Store.WidFactory.createWid(chatId); 246 | try { 247 | await window.Store.GroupUtils.setGroupSubject(chatWid, subject); 248 | return true; 249 | } catch (err) { 250 | if(err.name === 'ServerStatusCodeError') return false; 251 | throw err; 252 | } 253 | }, { chatId: this.id._serialized, subject }); 254 | 255 | if(!success) return false; 256 | this.name = subject; 257 | return true; 258 | } 259 | 260 | /** 261 | * Updates the group description 262 | * @param {string} description 263 | * @returns {Promise} Returns true if the description was properly updated. This can return false if the user does not have the necessary permissions. 264 | */ 265 | async setDescription(description) { 266 | const success = await this.client.mPage.evaluate(async ({chatId, description}) => { 267 | const chatWid = window.Store.WidFactory.createWid(chatId); 268 | let descId = window.Store.GroupMetadata.get(chatWid).descId; 269 | let newId = await window.Store.MsgKey.newId(); 270 | try { 271 | await window.Store.GroupUtils.setGroupDescription(chatWid, description, newId, descId); 272 | return true; 273 | } catch (err) { 274 | if(err.name === 'ServerStatusCodeError') return false; 275 | throw err; 276 | } 277 | }, { chatId: this.id._serialized, description}); 278 | 279 | if(!success) return false; 280 | this.groupMetadata.desc = description; 281 | return true; 282 | } 283 | 284 | /** 285 | * Updates the group settings to only allow admins to send messages. 286 | * @param {boolean} [adminsOnly=true] Enable or disable this option 287 | * @returns {Promise} Returns true if the setting was properly updated. This can return false if the user does not have the necessary permissions. 288 | */ 289 | async setMessagesAdminsOnly(adminsOnly=true) { 290 | const success = await this.client.mPage.evaluate(async ({chatId, adminsOnly}) => { 291 | const chatWid = window.Store.WidFactory.createWid(chatId); 292 | try { 293 | await window.Store.GroupUtils.setGroupProperty(chatWid, 'announcement', adminsOnly ? 1 : 0); 294 | return true; 295 | } catch (err) { 296 | if(err.name === 'ServerStatusCodeError') return false; 297 | throw err; 298 | } 299 | }, {chatId: this.id._serialized, adminsOnly}); 300 | 301 | if(!success) return false; 302 | 303 | this.groupMetadata.announce = adminsOnly; 304 | return true; 305 | } 306 | 307 | /** 308 | * Updates the group settings to only allow admins to edit group info (title, description, photo). 309 | * @param {boolean} [adminsOnly=true] Enable or disable this option 310 | * @returns {Promise} Returns true if the setting was properly updated. This can return false if the user does not have the necessary permissions. 311 | */ 312 | async setInfoAdminsOnly(adminsOnly=true) { 313 | const success = await this.client.mPage.evaluate(async ({chatId, adminsOnly}) => { 314 | const chatWid = window.Store.WidFactory.createWid(chatId); 315 | try { 316 | await window.Store.GroupUtils.setGroupProperty(chatWid, 'restrict', adminsOnly ? 1 : 0); 317 | return true; 318 | } catch (err) { 319 | if(err.name === 'ServerStatusCodeError') return false; 320 | throw err; 321 | } 322 | }, { chatId: this.id._serialized, adminsOnly }); 323 | 324 | if(!success) return false; 325 | 326 | this.groupMetadata.restrict = adminsOnly; 327 | return true; 328 | } 329 | 330 | /** 331 | * Deletes the group's picture. 332 | * @returns {Promise} Returns true if the picture was properly deleted. This can return false if the user does not have the necessary permissions. 333 | */ 334 | async deletePicture() { 335 | const success = await this.client.mPage.evaluate((chatid) => { 336 | return window.WWebJS.deletePicture(chatid); 337 | }, this.id._serialized); 338 | 339 | return success; 340 | } 341 | 342 | /** 343 | * Sets the group's picture. 344 | * @param {MessageMedia} media 345 | * @returns {Promise} Returns true if the picture was properly updated. This can return false if the user does not have the necessary permissions. 346 | */ 347 | async setPicture(media) { 348 | const success = await this.client.mPage.evaluate(({chatid, media}) => { 349 | return window.WWebJS.setPicture(chatid, media); 350 | }, {chatid: this.id._serialized, media}); 351 | 352 | return success; 353 | } 354 | 355 | /** 356 | * Gets the invite code for a specific group 357 | * @returns {Promise} Group's invite code 358 | */ 359 | async getInviteCode() { 360 | const codeRes = await this.client.mPage.evaluate(async chatId => { 361 | const chatWid = window.Store.WidFactory.createWid(chatId); 362 | return window.Store.GroupInvite.queryGroupInviteCode(chatWid); 363 | }, this.id._serialized); 364 | 365 | return codeRes.code; 366 | } 367 | 368 | /** 369 | * Invalidates the current group invite code and generates a new one 370 | * @returns {Promise} New invite code 371 | */ 372 | async revokeInvite() { 373 | const codeRes = await this.client.mPage.evaluate(chatId => { 374 | const chatWid = window.Store.WidFactory.createWid(chatId); 375 | return window.Store.GroupInvite.resetGroupInviteCode(chatWid); 376 | }, this.id._serialized); 377 | 378 | return codeRes.code; 379 | } 380 | 381 | /** 382 | * An object that handles the information about the group membership request 383 | * @typedef {Object} GroupMembershipRequest 384 | * @property {Object} id The wid of a user who requests to enter the group 385 | * @property {Object} addedBy The wid of a user who created that request 386 | * @property {Object|null} parentGroupId The wid of a community parent group to which the current group is linked 387 | * @property {string} requestMethod The method used to create the request: NonAdminAdd/InviteLink/LinkedGroupJoin 388 | * @property {number} t The timestamp the request was created at 389 | */ 390 | 391 | /** 392 | * Gets an array of membership requests 393 | * @returns {Promise>} An array of membership requests 394 | */ 395 | async getGroupMembershipRequests() { 396 | return await this.client.getGroupMembershipRequests(this.id._serialized); 397 | } 398 | 399 | /** 400 | * An object that handles the result for membership request action 401 | * @typedef {Object} MembershipRequestActionResult 402 | * @property {string} requesterId User ID whos membership request was approved/rejected 403 | * @property {number} error An error code that occurred during the operation for the participant 404 | * @property {string} message A message with a result of membership request action 405 | */ 406 | 407 | /** 408 | * An object that handles options for {@link approveGroupMembershipRequests} and {@link rejectGroupMembershipRequests} methods 409 | * @typedef {Object} MembershipRequestActionOptions 410 | * @property {Array|string|null} requesterIds User ID/s who requested to join the group, if no value is provided, the method will search for all membership requests for that group 411 | * @property {Array|number|null} sleep The number of milliseconds to wait before performing an operation for the next requester. If it is an array, a random sleep time between the sleep[0] and sleep[1] values will be added (the difference must be >=100 ms, otherwise, a random sleep time between sleep[1] and sleep[1] + 100 will be added). If sleep is a number, a sleep time equal to its value will be added. By default, sleep is an array with a value of [250, 500] 412 | */ 413 | 414 | /** 415 | * Approves membership requests if any 416 | * @param {MembershipRequestActionOptions} options Options for performing a membership request action 417 | * @returns {Promise>} 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 418 | */ 419 | async approveGroupMembershipRequests(options = {}) { 420 | return await this.client.approveGroupMembershipRequests(this.id._serialized, options); 421 | } 422 | 423 | /** 424 | * Rejects membership requests if any 425 | * @param {MembershipRequestActionOptions} options Options for performing a membership request action 426 | * @returns {Promise>} 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 427 | */ 428 | async rejectGroupMembershipRequests(options = {}) { 429 | return await this.client.rejectGroupMembershipRequests(this.id._serialized, options); 430 | } 431 | 432 | /** 433 | * Makes the bot leave the group 434 | * @returns {Promise} 435 | */ 436 | async leave() { 437 | await this.client.mPage.evaluate(async chatId => { 438 | const chatWid = window.Store.WidFactory.createWid(chatId); 439 | const chat = await window.Store.Chat.find(chatWid); 440 | return window.Store.GroupUtils.sendExitGroup(chat); 441 | }, this.id._serialized); 442 | } 443 | 444 | } 445 | 446 | module.exports = GroupChat; 447 | -------------------------------------------------------------------------------- /src/structures/GroupNotification.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./Base'); 4 | 5 | /** 6 | * Represents a GroupNotification on WhatsApp 7 | * @extends {Base} 8 | */ 9 | class GroupNotification extends Base { 10 | constructor(client, data) { 11 | super(client); 12 | 13 | if(data) this._patch(data); 14 | } 15 | 16 | _patch(data) { 17 | /** 18 | * ID that represents the groupNotification 19 | * @type {object} 20 | */ 21 | this.id = data.id; 22 | 23 | /** 24 | * Extra content 25 | * @type {string} 26 | */ 27 | this.body = data.body || ''; 28 | 29 | /** 30 | * GroupNotification type 31 | * @type {GroupNotificationTypes} 32 | */ 33 | this.type = data.subtype; 34 | 35 | /** 36 | * Unix timestamp for when the groupNotification was created 37 | * @type {number} 38 | */ 39 | this.timestamp = data.t; 40 | 41 | /** 42 | * ID for the Chat that this groupNotification was sent for. 43 | * 44 | * @type {string} 45 | */ 46 | this.chatId = typeof (data.id.remote) === 'object' ? data.id.remote._serialized : data.id.remote; 47 | 48 | /** 49 | * ContactId for the user that produced the GroupNotification. 50 | * @type {string} 51 | */ 52 | this.author = typeof (data.author) === 'object' ? data.author._serialized : data.author; 53 | 54 | /** 55 | * Contact IDs for the users that were affected by this GroupNotification. 56 | * @type {Array} 57 | */ 58 | this.recipientIds = []; 59 | 60 | if (data.recipients) { 61 | this.recipientIds = data.recipients; 62 | } 63 | 64 | return super._patch(data); 65 | } 66 | 67 | /** 68 | * Returns the Chat this groupNotification was sent in 69 | * @returns {Promise} 70 | */ 71 | getChat() { 72 | return this.client.getChatById(this.chatId); 73 | } 74 | 75 | /** 76 | * Returns the Contact this GroupNotification was produced by 77 | * @returns {Promise} 78 | */ 79 | getContact() { 80 | return this.client.getContactById(this.author); 81 | } 82 | 83 | /** 84 | * Returns the Contacts affected by this GroupNotification. 85 | * @returns {Promise>} 86 | */ 87 | async getRecipients() { 88 | return await Promise.all(this.recipientIds.map(async m => await this.client.getContactById(m))); 89 | } 90 | 91 | /** 92 | * Sends a message to the same chat this GroupNotification was produced in. 93 | * 94 | * @param {string|MessageMedia|Location} content 95 | * @param {object} options 96 | * @returns {Promise} 97 | */ 98 | async reply(content, options={}) { 99 | return this.client.sendMessage(this.chatId, content, options); 100 | } 101 | 102 | } 103 | 104 | module.exports = GroupNotification; 105 | -------------------------------------------------------------------------------- /src/structures/Label.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./Base'); 4 | // eslint-disable-next-line no-unused-vars 5 | const Chat = require('./Chat'); 6 | 7 | /** 8 | * WhatsApp Business Label information 9 | */ 10 | class Label extends Base { 11 | /** 12 | * @param {Base} client 13 | * @param {object} labelData 14 | */ 15 | constructor(client, labelData){ 16 | super(client); 17 | 18 | if(labelData) this._patch(labelData); 19 | } 20 | 21 | _patch(labelData){ 22 | /** 23 | * Label ID 24 | * @type {string} 25 | */ 26 | this.id = labelData.id; 27 | 28 | /** 29 | * Label name 30 | * @type {string} 31 | */ 32 | this.name = labelData.name; 33 | 34 | /** 35 | * Label hex color 36 | * @type {string} 37 | */ 38 | this.hexColor = labelData.hexColor; 39 | } 40 | /** 41 | * Get all chats that have been assigned this Label 42 | * @returns {Promise>} 43 | */ 44 | async getChats(){ 45 | return this.client.getChatsByLabelId(this.id); 46 | } 47 | 48 | } 49 | 50 | module.exports = Label; -------------------------------------------------------------------------------- /src/structures/List.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Util = require('../util/Util'); 4 | 5 | /** 6 | * Message type List 7 | */ 8 | class List { 9 | /** 10 | * @param {string} body 11 | * @param {string} buttonText 12 | * @param {Array} sections 13 | * @param {string?} title 14 | * @param {string?} footer 15 | */ 16 | constructor(body, buttonText, sections, title, footer) { 17 | /** 18 | * Message body 19 | * @type {string} 20 | */ 21 | this.description = body; 22 | 23 | /** 24 | * List button text 25 | * @type {string} 26 | */ 27 | this.buttonText = buttonText; 28 | 29 | /** 30 | * title of message 31 | * @type {string} 32 | */ 33 | this.title = title; 34 | 35 | 36 | /** 37 | * footer of message 38 | * @type {string} 39 | */ 40 | this.footer = footer; 41 | 42 | /** 43 | * sections of message 44 | * @type {Array} 45 | */ 46 | this.sections = this._format(sections); 47 | 48 | } 49 | 50 | /** 51 | * Creates section array from simple array 52 | * @param {Array} sections 53 | * @returns {Array} 54 | * @example 55 | * Input: [{title:'sectionTitle',rows:[{id:'customId', title:'ListItem2', description: 'desc'},{title:'ListItem2'}]}}] 56 | * Returns: [{'title':'sectionTitle','rows':[{'rowId':'customId','title':'ListItem1','description':'desc'},{'rowId':'oGSRoD','title':'ListItem2','description':''}]}] 57 | */ 58 | _format(sections){ 59 | if(!sections.length){throw '[LT02] List without sections';} 60 | if(sections.length > 1 && sections.filter(s => typeof s.title == 'undefined').length > 1){throw '[LT05] You can\'t have more than one empty title.';} 61 | return sections.map( (section) =>{ 62 | if(!section.rows.length){throw '[LT03] Section without rows';} 63 | return { 64 | title: section.title ? section.title : undefined, 65 | rows: section.rows.map( (row) => { 66 | if(!row.title){throw '[LT04] Row without title';} 67 | return { 68 | rowId: row.id ? row.id : Util.generateHash(6), 69 | title: row.title, 70 | description: row.description ? row.description : '' 71 | }; 72 | }) 73 | }; 74 | }); 75 | } 76 | 77 | } 78 | 79 | module.exports = List; 80 | -------------------------------------------------------------------------------- /src/structures/Location.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Location send options 5 | * @typedef {Object} LocationSendOptions 6 | * @property {string} [name] Location name 7 | * @property {string} [address] Location address 8 | * @property {string} [url] URL address to be shown within a location message 9 | */ 10 | 11 | /** 12 | * Location information 13 | */ 14 | class Location { 15 | /** 16 | * @param {number} latitude 17 | * @param {number} longitude 18 | * @param {LocationSendOptions} [options] Location send options 19 | */ 20 | constructor(latitude, longitude, options = {}) { 21 | /** 22 | * Location latitude 23 | * @type {number} 24 | */ 25 | this.latitude = latitude; 26 | 27 | /** 28 | * Location longitude 29 | * @type {number} 30 | */ 31 | this.longitude = longitude; 32 | 33 | /** 34 | * Name for the location 35 | * @type {string|undefined} 36 | */ 37 | this.name = options.name; 38 | 39 | /** 40 | * Location address 41 | * @type {string|undefined} 42 | */ 43 | this.address = options.address; 44 | 45 | /** 46 | * URL address to be shown within a location message 47 | * @type {string|undefined} 48 | */ 49 | this.url = options.url; 50 | 51 | /** 52 | * Location full description 53 | * @type {string|undefined} 54 | */ 55 | this.description = this.name && this.address 56 | ? `${this.name}\n${this.address}` 57 | : this.name || this.address || ''; 58 | } 59 | } 60 | 61 | module.exports = Location; -------------------------------------------------------------------------------- /src/structures/Message.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Base = require("./Base"); 4 | const MessageMedia = require("./MessageMedia"); 5 | const Location = require("./Location"); 6 | const Order = require("./Order"); 7 | const Payment = require("./Payment"); 8 | const Reaction = require("./Reaction"); 9 | const { MessageTypes } = require("../util/Constants"); 10 | const { Contact } = require("./Contact"); 11 | 12 | /** 13 | * Represents a Message on WhatsApp 14 | * @extends {Base} 15 | */ 16 | class Message extends Base { 17 | constructor(client, data) { 18 | super(client); 19 | 20 | if (data) this._patch(data); 21 | } 22 | 23 | _patch(data) { 24 | this._data = data; 25 | 26 | /** 27 | * MediaKey that represents the sticker 'ID' 28 | * @type {string} 29 | */ 30 | this.mediaKey = data.mediaKey; 31 | 32 | /** 33 | * ID that represents the message 34 | * @type {object} 35 | */ 36 | this.id = data.id; 37 | 38 | /** 39 | * ACK status for the message 40 | * @type {MessageAck} 41 | */ 42 | this.ack = data.ack; 43 | 44 | /** 45 | * Indicates if the message has media available for download 46 | * @type {boolean} 47 | */ 48 | this.hasMedia = Boolean(data.mediaKey && data.directPath); 49 | 50 | /** 51 | * Message content 52 | * @type {string} 53 | */ 54 | this.body = this.hasMedia 55 | ? data.caption || "" 56 | : data.body || data.pollName || ""; 57 | 58 | /** 59 | * Message type 60 | * @type {MessageTypes} 61 | */ 62 | this.type = data.type; 63 | 64 | /** 65 | * Unix timestamp for when the message was created 66 | * @type {number} 67 | */ 68 | this.timestamp = data.t; 69 | 70 | /** 71 | * ID for the Chat that this message was sent to, except if the message was sent by the current user. 72 | * @type {string} 73 | */ 74 | this.from = 75 | typeof data.from === "object" && data.from !== null 76 | ? data.from._serialized 77 | : data.from; 78 | 79 | /** 80 | * ID for who this message is for. 81 | * 82 | * If the message is sent by the current user, it will be the Chat to which the message is being sent. 83 | * If the message is sent by another user, it will be the ID for the current user. 84 | * @type {string} 85 | */ 86 | this.to = 87 | typeof data.to === "object" && data.to !== null 88 | ? data.to._serialized 89 | : data.to; 90 | 91 | /** 92 | * If the message was sent to a group, this field will contain the user that sent the message. 93 | * @type {string} 94 | */ 95 | this.author = 96 | typeof data.author === "object" && data.author !== null 97 | ? data.author._serialized 98 | : data.author; 99 | 100 | /** 101 | * String that represents from which device type the message was sent 102 | * @type {string} 103 | */ 104 | this.deviceType = 105 | typeof data.id.id === "string" && data.id.id.length > 21 106 | ? "android" 107 | : typeof data.id.id === "string" && data.id.id.substring(0, 2) === "3A" 108 | ? "ios" 109 | : "web"; 110 | /** 111 | * Indicates if the message was forwarded 112 | * @type {boolean} 113 | */ 114 | this.isForwarded = data.isForwarded; 115 | 116 | /** 117 | * Indicates how many times the message was forwarded. 118 | * 119 | * The maximum value is 127. 120 | * @type {number} 121 | */ 122 | this.forwardingScore = data.forwardingScore || 0; 123 | 124 | /** 125 | * Indicates if the message is a status update 126 | * @type {boolean} 127 | */ 128 | this.isStatus = data.isStatusV3 || data.id.remote === "status@broadcast"; 129 | 130 | /** 131 | * Indicates if the message was starred 132 | * @type {boolean} 133 | */ 134 | this.isStarred = data.star; 135 | 136 | /** 137 | * Indicates if the message was a broadcast 138 | * @type {boolean} 139 | */ 140 | this.broadcast = data.broadcast; 141 | 142 | /** 143 | * Indicates if the message was sent by the current user 144 | * @type {boolean} 145 | */ 146 | this.fromMe = data.id.fromMe; 147 | 148 | /** 149 | * Indicates if the message was sent as a reply to another message. 150 | * @type {boolean} 151 | */ 152 | this.hasQuotedMsg = data.quotedMsg ? true : false; 153 | 154 | /** 155 | * Indicates whether there are reactions to the message 156 | * @type {boolean} 157 | */ 158 | this.hasReaction = data.hasReaction ? true : false; 159 | 160 | /** 161 | * Indicates the duration of the message in seconds 162 | * @type {string} 163 | */ 164 | this.duration = data.duration ? data.duration : undefined; 165 | 166 | /** 167 | * Location information contained in the message, if the message is type "location" 168 | * @type {Location} 169 | */ 170 | this.location = (() => { 171 | if (data.type !== MessageTypes.LOCATION) { 172 | return undefined; 173 | } 174 | let description; 175 | if (data.loc && typeof data.loc === "string") { 176 | let splitted = data.loc.split("\n"); 177 | description = { 178 | name: splitted[0], 179 | address: splitted[1], 180 | url: data.clientUrl, 181 | }; 182 | } 183 | return new Location(data.lat, data.lng, description); 184 | })(); 185 | 186 | /** 187 | * List of vCards contained in the message. 188 | * @type {Array} 189 | */ 190 | this.vCards = 191 | data.type === MessageTypes.CONTACT_CARD_MULTI 192 | ? data.vcardList.map((c) => c.vcard) 193 | : data.type === MessageTypes.CONTACT_CARD 194 | ? [data.body] 195 | : []; 196 | 197 | /** 198 | * Group Invite Data 199 | * @type {object} 200 | */ 201 | this.inviteV4 = 202 | data.type === MessageTypes.GROUP_INVITE 203 | ? { 204 | inviteCode: data.inviteCode, 205 | inviteCodeExp: data.inviteCodeExp, 206 | groupId: data.inviteGrp, 207 | groupName: data.inviteGrpName, 208 | fromId: 209 | "_serialized" in data.from ? data.from._serialized : data.from, 210 | toId: "_serialized" in data.to ? data.to._serialized : data.to, 211 | } 212 | : undefined; 213 | 214 | /** 215 | * Indicates the mentions in the message body. 216 | * @type {Array} 217 | */ 218 | this.mentionedIds = []; 219 | 220 | if (data.mentionedJidList) { 221 | this.mentionedIds = data.mentionedJidList; 222 | } 223 | 224 | this.groupMentions = data.groupMentions || []; 225 | 226 | /** 227 | * Order ID for message type ORDER 228 | * @type {string} 229 | */ 230 | this.orderId = data.orderId ? data.orderId : undefined; 231 | /** 232 | * Order Token for message type ORDER 233 | * @type {string} 234 | */ 235 | this.token = data.token ? data.token : undefined; 236 | 237 | /** 238 | * Indicates whether the message is a Gif 239 | * @type {boolean} 240 | */ 241 | this.isGif = Boolean(data.isGif); 242 | 243 | /** 244 | * Indicates if the message will disappear after it expires 245 | * @type {boolean} 246 | */ 247 | this.isEphemeral = data.isEphemeral; 248 | 249 | /** Title */ 250 | if (data.title) { 251 | this.title = data.title; 252 | } 253 | 254 | /** Description */ 255 | if (data.description) { 256 | this.description = data.description; 257 | } 258 | 259 | /** Business Owner JID */ 260 | if (data.businessOwnerJid) { 261 | this.businessOwnerJid = data.businessOwnerJid; 262 | } 263 | 264 | /** Product ID */ 265 | if (data.productId) { 266 | this.productId = data.productId; 267 | } 268 | 269 | /** Last edit time */ 270 | if (data.latestEditSenderTimestampMs) { 271 | this.latestEditSenderTimestampMs = data.latestEditSenderTimestampMs; 272 | } 273 | 274 | /** Last edit message author */ 275 | if (data.latestEditMsgKey) { 276 | this.latestEditMsgKey = data.latestEditMsgKey; 277 | } 278 | 279 | /** 280 | * Links included in the message. 281 | * @type {Array<{link: string, isSuspicious: boolean}>} 282 | * 283 | */ 284 | this.links = data.links; 285 | 286 | /** Buttons */ 287 | if (data.dynamicReplyButtons) { 288 | this.dynamicReplyButtons = data.dynamicReplyButtons; 289 | } 290 | 291 | /** Selected Button Id **/ 292 | if (data.selectedButtonId) { 293 | this.selectedButtonId = data.selectedButtonId; 294 | } 295 | 296 | /** Selected List row Id **/ 297 | if ( 298 | data.listResponse && 299 | data.listResponse.singleSelectReply.selectedRowId 300 | ) { 301 | this.selectedRowId = data.listResponse.singleSelectReply.selectedRowId; 302 | } 303 | 304 | if (this.type === MessageTypes.POLL_CREATION) { 305 | this.pollName = data.pollName; 306 | this.pollOptions = data.pollOptions; 307 | this.allowMultipleAnswers = Boolean(!data.pollSelectableOptionsCount); 308 | this.pollInvalidated = data.pollInvalidated; 309 | this.isSentCagPollCreation = data.isSentCagPollCreation; 310 | 311 | delete this._data.pollName; 312 | delete this._data.pollOptions; 313 | delete this._data.pollSelectableOptionsCount; 314 | delete this._data.pollInvalidated; 315 | delete this._data.isSentCagPollCreation; 316 | } 317 | 318 | return super._patch(data); 319 | } 320 | 321 | _getChatId() { 322 | return this.fromMe ? this.to : this.from; 323 | } 324 | 325 | /** 326 | * Reloads this Message object's data in-place with the latest values from WhatsApp Web. 327 | * Note that the Message must still be in the web app cache for this to work, otherwise will return null. 328 | * @returns {Promise} 329 | */ 330 | async reload() { 331 | const newData = await this.client.mPage.evaluate((msgId) => { 332 | const msg = window.Store.Msg.get(msgId); 333 | if (!msg) return null; 334 | return window.WWebJS.getMessageModel(msg); 335 | }, this.id._serialized); 336 | 337 | if (!newData) return null; 338 | 339 | this._patch(newData); 340 | return this; 341 | } 342 | 343 | /** 344 | * Returns message in a raw format 345 | * @type {Object} 346 | */ 347 | get rawData() { 348 | return this._data; 349 | } 350 | 351 | /** 352 | * Returns the Chat this message was sent in 353 | * @returns {Promise} 354 | */ 355 | getChat() { 356 | return this.client.getChatById(this._getChatId()); 357 | } 358 | 359 | /** 360 | * Returns the Contact this message was sent from 361 | * @returns {Promise} 362 | */ 363 | getContact() { 364 | return this.client.getContactById(this.author || this.from); 365 | } 366 | 367 | /** 368 | * Returns the Contacts mentioned in this message 369 | * @returns {Promise>} 370 | */ 371 | async getMentions() { 372 | return await Promise.all( 373 | this.mentionedIds.map(async (m) => await this.client.getContactById(m)) 374 | ); 375 | } 376 | 377 | /** 378 | * Returns groups mentioned in this message 379 | * @returns {Promise} 380 | */ 381 | async getGroupMentions() { 382 | return await Promise.all(this.groupMentions.map(async (m) => await this.client.getChatById(m.groupJid._serialized))); 383 | } 384 | 385 | /** 386 | * Returns the quoted message, if any 387 | * @returns {Promise} 388 | */ 389 | async getQuotedMessage() { 390 | if (!this.hasQuotedMsg) return undefined; 391 | 392 | const quotedMsg = await this.client.mPage.evaluate((msgId) => { 393 | const msg = window.Store.Msg.get(msgId); 394 | const quotedMsg = window.Store.QuotedMsg.getQuotedMsgObj(msg); 395 | return window.WWebJS.getMessageModel(quotedMsg); 396 | }, this.id._serialized); 397 | 398 | return new Message(this.client, quotedMsg); 399 | } 400 | 401 | /** 402 | * Sends a message as a reply to this message. If chatId is specified, it will be sent 403 | * through the specified Chat. If not, it will send the message 404 | * in the same Chat as the original message was sent. 405 | * 406 | * @param {string|MessageMedia|Location} content 407 | * @param {string} [chatId] 408 | * @param {MessageSendOptions} [options] 409 | * @returns {Promise} 410 | */ 411 | async reply(content, chatId, options = {}) { 412 | if (!chatId) { 413 | chatId = this._getChatId(); 414 | } 415 | 416 | options = { 417 | ...options, 418 | quoted: this.id._serialized 419 | }; 420 | 421 | return this.client.sendMessage(chatId, content, options); 422 | } 423 | 424 | /** 425 | * React to this message with an emoji 426 | * @param {string} reaction - Emoji to react with. Send an empty string to remove the reaction. 427 | * @return {Promise} 428 | */ 429 | async react(reaction) { 430 | await this.client.mPage.evaluate( 431 | async ({ messageId, reaction }) => { 432 | if (!messageId) { 433 | return undefined; 434 | } 435 | 436 | const msg = await window.Store.Msg.get(messageId); 437 | await window.Store.sendReactionToMsg(msg, reaction); 438 | }, 439 | { messageId: this.id._serialized, reaction } 440 | ); 441 | } 442 | 443 | /** 444 | * Accept Group V4 Invite 445 | * @returns {Promise} 446 | */ 447 | async acceptGroupV4Invite() { 448 | return await this.client.acceptGroupV4Invite(this.inviteV4); 449 | } 450 | 451 | /** 452 | * Forwards this message to another chat (that you chatted before, otherwise it will fail) 453 | * 454 | * @param {string|Chat} chat Chat model or chat ID to which the message will be forwarded 455 | * @returns {Promise} 456 | */ 457 | async forward(chat) { 458 | const chatId = typeof chat === "string" ? chat : chat.id._serialized; 459 | 460 | await this.client.mPage.evaluate( 461 | async ({ msgId, chatId }) => { 462 | let msg = window.Store.Msg.get(msgId); 463 | let chat = window.Store.Chat.get(chatId); 464 | 465 | return await chat.forwardMessages([msg]); 466 | }, 467 | { msgId: this.id._serialized, chatId } 468 | ); 469 | } 470 | 471 | /** 472 | * Downloads and returns the attatched message media 473 | * @returns {Promise} 474 | */ 475 | async downloadMedia() { 476 | if (!this.hasMedia) { 477 | return undefined; 478 | } 479 | 480 | const result = await this.client.mPage.evaluate(async (msgId) => { 481 | const msg = window.Store.Msg.get(msgId); 482 | if (!msg) { 483 | return undefined; 484 | } 485 | if (msg.mediaData.mediaStage != "RESOLVED") { 486 | // try to resolve media 487 | await msg.downloadMedia({ 488 | downloadEvenIfExpensive: true, 489 | rmrReason: 1, 490 | }); 491 | } 492 | 493 | if ( 494 | msg.mediaData.mediaStage.includes("ERROR") || 495 | msg.mediaData.mediaStage === "FETCHING" 496 | ) { 497 | // media could not be downloaded 498 | return undefined; 499 | } 500 | 501 | try { 502 | const decryptedMedia = 503 | await window.Store.DownloadManager.downloadAndMaybeDecrypt({ 504 | directPath: msg.directPath, 505 | encFilehash: msg.encFilehash, 506 | filehash: msg.filehash, 507 | mediaKey: msg.mediaKey, 508 | mediaKeyTimestamp: msg.mediaKeyTimestamp, 509 | type: msg.type, 510 | signal: new AbortController().signal, 511 | }); 512 | 513 | const data = await window.WWebJS.arrayBufferToBase64Async( 514 | decryptedMedia 515 | ); 516 | 517 | return { 518 | data, 519 | mimetype: msg.mimetype, 520 | filename: msg.filename, 521 | filesize: msg.size, 522 | }; 523 | } catch (e) { 524 | if (e.status && e.status === 404) return undefined; 525 | throw e; 526 | } 527 | }, this.id._serialized); 528 | 529 | if (!result) return undefined; 530 | return new MessageMedia( 531 | result.mimetype, 532 | result.data, 533 | result.filename, 534 | result.filesize 535 | ); 536 | } 537 | 538 | /** 539 | * Deletes a message from the chat 540 | * @param {?boolean} everyone If true and the message is sent by the current user or the user is an admin, will delete it for everyone in the chat. 541 | */ 542 | async delete(everyone) { 543 | await this.client.mPage.evaluate( 544 | async ({ msgId, everyone }) => { 545 | let msg = window.Store.Msg.get(msgId); 546 | let chat = await window.Store.Chat.find(msg.id.remote); 547 | 548 | const canRevoke = 549 | window.Store.MsgActionChecks.canSenderRevokeMsg(msg) || 550 | window.Store.MsgActionChecks.canAdminRevokeMsg(msg); 551 | if (everyone && canRevoke) { 552 | return window.Store.Cmd.sendRevokeMsgs(chat, [msg], { 553 | clearMedia: true, 554 | type: msg.id.fromMe ? "Sender" : "Admin", 555 | }); 556 | } 557 | 558 | return window.Store.Cmd.sendDeleteMsgs(chat, [msg], true); 559 | }, 560 | { msgId: this.id._serialized, everyone } 561 | ); 562 | } 563 | 564 | /** 565 | * Stars this message 566 | */ 567 | async star() { 568 | await this.client.mPage.evaluate(async (msgId) => { 569 | let msg = window.Store.Msg.get(msgId); 570 | 571 | if (window.Store.MsgActionChecks.canStarMsg(msg)) { 572 | let chat = await window.Store.Chat.find(msg.id.remote); 573 | return window.Store.Cmd.sendStarMsgs(chat, [msg], false); 574 | } 575 | }, this.id._serialized); 576 | } 577 | 578 | /** 579 | * Unstars this message 580 | */ 581 | async unstar() { 582 | await this.client.mPage.evaluate(async (msgId) => { 583 | let msg = window.Store.Msg.get(msgId); 584 | 585 | if (window.Store.MsgActionChecks.canStarMsg(msg)) { 586 | let chat = await window.Store.Chat.find(msg.id.remote); 587 | return window.Store.Cmd.sendUnstarMsgs(chat, [msg], false); 588 | } 589 | }, this.id._serialized); 590 | } 591 | 592 | /** 593 | * Message Info 594 | * @typedef {Object} MessageInfo 595 | * @property {Array<{id: ContactId, t: number}>} delivery Contacts to which the message has been delivered to 596 | * @property {number} deliveryRemaining Amount of people to whom the message has not been delivered to 597 | * @property {Array<{id: ContactId, t: number}>} played Contacts who have listened to the voice message 598 | * @property {number} playedRemaining Amount of people who have not listened to the message 599 | * @property {Array<{id: ContactId, t: number}>} read Contacts who have read the message 600 | * @property {number} readRemaining Amount of people who have not read the message 601 | */ 602 | 603 | /** 604 | * Get information about message delivery status. May return null if the message does not exist or is not sent by you. 605 | * @returns {Promise} 606 | */ 607 | async getInfo() { 608 | const info = await this.client.mPage.evaluate(async (msgId) => { 609 | const msg = window.Store.Msg.get(msgId); 610 | if (!msg) return null; 611 | 612 | return await window.Store.MessageInfo.sendQueryMsgInfo(msg.id); 613 | }, this.id._serialized); 614 | 615 | return info; 616 | } 617 | 618 | /** 619 | * Gets the order associated with a given message 620 | * @return {Promise} 621 | */ 622 | async getOrder() { 623 | if (this.type === MessageTypes.ORDER) { 624 | const result = await this.client.mPage.evaluate( 625 | ({ orderId, token, chatId }) => { 626 | return window.WWebJS.getOrderDetail(orderId, token, chatId); 627 | }, 628 | { orderId: this.orderId, token: this.token, chatId: this._getChatId() } 629 | ); 630 | if (!result) return undefined; 631 | return new Order(this.client, result); 632 | } 633 | return undefined; 634 | } 635 | /** 636 | * Gets the payment details associated with a given message 637 | * @return {Promise} 638 | */ 639 | async getPayment() { 640 | if (this.type === MessageTypes.PAYMENT) { 641 | const msg = await this.client.mPage.evaluate(async (msgId) => { 642 | const msg = window.Store.Msg.get(msgId); 643 | if (!msg) return null; 644 | return msg.serialize(); 645 | }, this.id._serialized); 646 | return new Payment(this.client, msg); 647 | } 648 | return undefined; 649 | } 650 | 651 | /** 652 | * Reaction List 653 | * @typedef {Object} ReactionList 654 | * @property {string} id Original emoji 655 | * @property {string} aggregateEmoji aggregate emoji 656 | * @property {boolean} hasReactionByMe Flag who sent the reaction 657 | * @property {Array} senders Reaction senders, to this message 658 | */ 659 | 660 | /** 661 | * Gets the reactions associated with the given message 662 | * @return {Promise} 663 | */ 664 | async getReactions() { 665 | if (!this.hasReaction) { 666 | return undefined; 667 | } 668 | 669 | const reactions = await this.client.mPage.evaluate(async (msgId) => { 670 | const msgReactions = await window.Store.Reactions.find(msgId); 671 | if (!msgReactions || !msgReactions.reactions.length) return null; 672 | return msgReactions.reactions.serialize(); 673 | }, this.id._serialized); 674 | 675 | if (!reactions) { 676 | return undefined; 677 | } 678 | 679 | return reactions.map((reaction) => { 680 | reaction.senders = reaction.senders.map((sender) => { 681 | sender.timestamp = Math.round(sender.timestamp / 1000); 682 | return new Reaction(this.client, sender); 683 | }); 684 | return reaction; 685 | }); 686 | } 687 | 688 | /** 689 | * Edits the current message. 690 | * @param {string} content 691 | * @param {MessageEditOptions} [options] - Options used when editing the message 692 | * @returns {Promise} 693 | */ 694 | async edit(content, options = {}) { 695 | if ( 696 | options.mentions && 697 | options.mentions.some( 698 | (possiblyContact) => possiblyContact instanceof Contact 699 | ) 700 | ) { 701 | options.mentions = options.mentions.map((a) => a.id._serialized); 702 | } 703 | let internalOptions = { 704 | linkPreview: options.linkPreview === false ? undefined : true, 705 | mentionedJidList: Array.isArray(options.mentions) ? options.mentions : [], 706 | extraOptions: options.extra, 707 | }; 708 | 709 | if (!this.fromMe) { 710 | return null; 711 | } 712 | const messageEdit = await this.client.mPage.evaluate( 713 | async ({ msgId, message, options }) => { 714 | let msg = window.Store.Msg.get(msgId); 715 | if (!msg) return null; 716 | 717 | let catEdit = window.Store.MsgActionChecks.canEditText(msg) || window.Store.MsgActionChecks.canEditCaption(msg); 718 | if (catEdit) { 719 | const msgEdit = await window.WWebJS.editMessage( 720 | msg, 721 | message, 722 | options 723 | ); 724 | return msgEdit.serialize(); 725 | } 726 | return null; 727 | }, 728 | { msgId: this.id._serialized, content, internalOptions } 729 | ); 730 | if (messageEdit) { 731 | return new Message(this.client, messageEdit); 732 | } 733 | return null; 734 | } 735 | } 736 | 737 | module.exports = Message; 738 | -------------------------------------------------------------------------------- /src/structures/MessageMedia.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | const mime = require("mime"); 6 | const fetch = require("node-fetch"); 7 | const { URL } = require("url"); 8 | 9 | /** 10 | * Media attached to a message 11 | * @param {string} mimetype MIME type of the attachment 12 | * @param {string} data Base64-encoded data of the file 13 | * @param {?string} filename Document file name. Value can be null 14 | * @param {?number} filesize Document file size in bytes. Value can be null 15 | */ 16 | class MessageMedia { 17 | constructor(mimetype, data, filename, filesize) { 18 | /** 19 | * MIME type of the attachment 20 | * @type {string} 21 | */ 22 | this.mimetype = mimetype; 23 | 24 | /** 25 | * Base64 encoded data that represents the file 26 | * @type {string} 27 | */ 28 | this.data = data; 29 | 30 | /** 31 | * Document file name. Value can be null 32 | * @type {?string} 33 | */ 34 | this.filename = filename; 35 | 36 | /** 37 | * Document file size in bytes. Value can be null 38 | * @type {?number} 39 | */ 40 | this.filesize = filesize; 41 | } 42 | 43 | /** 44 | * Creates a MessageMedia instance from a local file path 45 | * @param {string} filePath 46 | * @returns {MessageMedia} 47 | */ 48 | static fromFilePath(filePath) { 49 | const b64data = fs.readFileSync(filePath, { encoding: "base64" }); 50 | const mimetype = mime.getType(filePath); 51 | const filename = path.basename(filePath); 52 | 53 | return new MessageMedia(mimetype, b64data, filename); 54 | } 55 | 56 | /** 57 | * Creates a MessageMedia instance from a URL 58 | * @param {string} url 59 | * @param {Object} [options] 60 | * @param {boolean} [options.unsafeMime=false] 61 | * @param {string} [options.filename] 62 | * @param {object} [options.client] 63 | * @param {object} [options.reqOptions] 64 | * @param {number} [options.reqOptions.size=0] 65 | * @returns {Promise} 66 | */ 67 | static async fromUrl(url, options = {}) { 68 | const pUrl = new URL(url); 69 | let mimetype = mime.getType(pUrl.pathname); 70 | 71 | if (!mimetype && !options.unsafeMime) 72 | throw new Error( 73 | "Unable to determine MIME type using URL. Set unsafeMime to true to download it anyway." 74 | ); 75 | 76 | async function fetchData(url, options) { 77 | const reqOptions = Object.assign( 78 | { headers: { accept: "image/* video/* text/* audio/*" } }, 79 | options 80 | ); 81 | const response = await fetch(url, reqOptions); 82 | const mime = response.headers.get("Content-Type"); 83 | const size = response.headers.get("Content-Length"); 84 | 85 | const contentDisposition = response.headers.get("Content-Disposition"); 86 | const name = contentDisposition 87 | ? contentDisposition.match(/((?<=filename=")(.*)(?="))/) 88 | : null; 89 | 90 | let data = ""; 91 | if (response.buffer) { 92 | data = (await response.buffer()).toString("base64"); 93 | } else { 94 | const bArray = new Uint8Array(await response.arrayBuffer()); 95 | bArray.forEach((b) => { 96 | data += String.fromCharCode(b); 97 | }); 98 | data = btoa(data); 99 | } 100 | 101 | return { data, mime, name, size }; 102 | } 103 | 104 | const res = options.client 105 | ? await options.client.mPage.evaluate(fetchData, url, options.reqOptions) 106 | : await fetchData(url, options.reqOptions); 107 | 108 | const filename = 109 | options.filename || 110 | (res.name ? res.name[0] : pUrl.pathname.split("/").pop() || "file"); 111 | 112 | if (!mimetype) mimetype = res.mime; 113 | 114 | return new MessageMedia(mimetype, res.data, filename, res.size || null); 115 | } 116 | } 117 | 118 | module.exports = MessageMedia; 119 | -------------------------------------------------------------------------------- /src/structures/Order.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./Base'); 4 | const Product = require('./Product'); 5 | 6 | /** 7 | * Represents a Order on WhatsApp 8 | * @extends {Base} 9 | */ 10 | class Order extends Base { 11 | constructor(client, data) { 12 | super(client); 13 | 14 | if (data) this._patch(data); 15 | } 16 | 17 | _patch(data) { 18 | /** 19 | * List of products 20 | * @type {Array} 21 | */ 22 | if (data.products) { 23 | this.products = data.products.map(product => new Product(this.client, product)); 24 | } 25 | /** 26 | * Order Subtotal 27 | * @type {string} 28 | */ 29 | this.subtotal = data.subtotal; 30 | /** 31 | * Order Total 32 | * @type {string} 33 | */ 34 | this.total = data.total; 35 | /** 36 | * Order Currency 37 | * @type {string} 38 | */ 39 | this.currency = data.currency; 40 | /** 41 | * Order Created At 42 | * @type {number} 43 | */ 44 | this.createdAt = data.createdAt; 45 | 46 | return super._patch(data); 47 | } 48 | 49 | 50 | } 51 | 52 | module.exports = Order; -------------------------------------------------------------------------------- /src/structures/Payment.js: -------------------------------------------------------------------------------- 1 | const Base = require('./Base'); 2 | 3 | class Payment extends Base { 4 | constructor(client, data) { 5 | super(client); 6 | 7 | if (data) this._patch(data); 8 | } 9 | 10 | _patch(data) { 11 | /** 12 | * The payment Id 13 | * @type {object} 14 | */ 15 | this.id = data.id; 16 | 17 | /** 18 | * The payment currency 19 | * @type {string} 20 | */ 21 | this.paymentCurrency = data.paymentCurrency; 22 | 23 | /** 24 | * The payment ammount ( R$ 1.00 = 1000 ) 25 | * @type {number} 26 | */ 27 | this.paymentAmount1000 = data.paymentAmount1000; 28 | 29 | /** 30 | * The payment receiver 31 | * @type {object} 32 | */ 33 | this.paymentMessageReceiverJid = data.paymentMessageReceiverJid; 34 | 35 | /** 36 | * The payment transaction timestamp 37 | * @type {number} 38 | */ 39 | this.paymentTransactionTimestamp = data.paymentTransactionTimestamp; 40 | 41 | /** 42 | * The paymentStatus 43 | * 44 | * Possible Status 45 | * 0:UNKNOWN_STATUS 46 | * 1:PROCESSING 47 | * 2:SENT 48 | * 3:NEED_TO_ACCEPT 49 | * 4:COMPLETE 50 | * 5:COULD_NOT_COMPLETE 51 | * 6:REFUNDED 52 | * 7:EXPIRED 53 | * 8:REJECTED 54 | * 9:CANCELLED 55 | * 10:WAITING_FOR_PAYER 56 | * 11:WAITING 57 | * 58 | * @type {number} 59 | */ 60 | this.paymentStatus = data.paymentStatus; 61 | 62 | /** 63 | * Integer that represents the payment Text 64 | * @type {number} 65 | */ 66 | this.paymentTxnStatus = data.paymentTxnStatus; 67 | 68 | /** 69 | * The note sent with the payment 70 | * @type {string} 71 | */ 72 | this.paymentNote = !data.paymentNoteMsg ? undefined : data.paymentNoteMsg.body ? data.paymentNoteMsg.body : undefined ; 73 | 74 | return super._patch(data); 75 | } 76 | 77 | } 78 | 79 | module.exports = Payment; 80 | -------------------------------------------------------------------------------- /src/structures/Poll.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Poll send options 5 | * @typedef {Object} PollSendOptions 6 | * @property {boolean} [allowMultipleAnswers=false] If false it is a single choice poll, otherwise it is a multiple choice poll (false by default) 7 | * @property {?Array} messageSecret The custom message secret, can be used as a poll ID. NOTE: it has to be a unique vector with a length of 32 8 | */ 9 | 10 | /** Represents a Poll on WhatsApp */ 11 | class Poll { 12 | /** 13 | * @param {string} pollName 14 | * @param {Array} pollOptions 15 | * @param {PollSendOptions} options 16 | */ 17 | constructor(pollName, pollOptions, options = {}) { 18 | /** 19 | * The name of the poll 20 | * @type {string} 21 | */ 22 | this.pollName = pollName.trim(); 23 | 24 | /** 25 | * The array of poll options 26 | * @type {Array.<{name: string, localId: number}>} 27 | */ 28 | this.pollOptions = pollOptions.map((option, index) => ({ 29 | name: option.trim(), 30 | localId: index 31 | })); 32 | 33 | /** 34 | * The send options for the poll 35 | * @type {PollSendOptions} 36 | */ 37 | this.options = { 38 | allowMultipleAnswers: options.allowMultipleAnswers === true, 39 | messageSecret: options.messageSecret 40 | }; 41 | } 42 | } 43 | 44 | module.exports = Poll; 45 | -------------------------------------------------------------------------------- /src/structures/PrivateChat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Chat = require('./Chat'); 4 | 5 | /** 6 | * Represents a Private Chat on WhatsApp 7 | * @extends {Chat} 8 | */ 9 | class PrivateChat extends Chat { 10 | 11 | } 12 | 13 | module.exports = PrivateChat; -------------------------------------------------------------------------------- /src/structures/PrivateContact.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Contact = require('./Contact'); 4 | 5 | /** 6 | * Represents a Private Contact on WhatsApp 7 | * @extends {Contact} 8 | */ 9 | class PrivateContact extends Contact { 10 | 11 | } 12 | 13 | module.exports = PrivateContact; -------------------------------------------------------------------------------- /src/structures/Product.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./Base'); 4 | const ProductMetadata = require('./ProductMetadata'); 5 | 6 | /** 7 | * Represents a Product on WhatsAppBusiness 8 | * @extends {Base} 9 | */ 10 | class Product extends Base { 11 | constructor(client, data) { 12 | super(client); 13 | 14 | if (data) this._patch(data); 15 | } 16 | 17 | _patch(data) { 18 | /** 19 | * Product ID 20 | * @type {string} 21 | */ 22 | this.id = data.id; 23 | /** 24 | * Price 25 | * @type {string} 26 | */ 27 | this.price = data.price ? data.price : ''; 28 | /** 29 | * Product Thumbnail 30 | * @type {string} 31 | */ 32 | this.thumbnailUrl = data.thumbnailUrl; 33 | /** 34 | * Currency 35 | * @type {string} 36 | */ 37 | this.currency = data.currency; 38 | /** 39 | * Product Name 40 | * @type {string} 41 | */ 42 | this.name = data.name; 43 | /** 44 | * Product Quantity 45 | * @type {number} 46 | */ 47 | this.quantity = data.quantity; 48 | /** Product metadata */ 49 | this.data = null; 50 | return super._patch(data); 51 | } 52 | 53 | async getData() { 54 | if (this.data === null) { 55 | let result = await this.client.mPage.evaluate((productId) => { 56 | return window.WWebJS.getProductMetadata(productId); 57 | }, this.id); 58 | if (!result) { 59 | this.data = undefined; 60 | } else { 61 | this.data = new ProductMetadata(this.client, result); 62 | } 63 | } 64 | return this.data; 65 | } 66 | } 67 | 68 | module.exports = Product; -------------------------------------------------------------------------------- /src/structures/ProductMetadata.js: -------------------------------------------------------------------------------- 1 | const Base = require('./Base'); 2 | 3 | class ProductMetadata extends Base { 4 | constructor(client, data) { 5 | super(client); 6 | 7 | if (data) this._patch(data); 8 | } 9 | 10 | _patch(data) { 11 | /** Product ID */ 12 | this.id = data.id; 13 | /** Retailer ID */ 14 | this.retailer_id = data.retailer_id; 15 | /** Product Name */ 16 | this.name = data.name; 17 | /** Product Description */ 18 | this.description = data.description; 19 | 20 | return super._patch(data); 21 | } 22 | 23 | } 24 | 25 | module.exports = ProductMetadata; -------------------------------------------------------------------------------- /src/structures/Reaction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./Base'); 4 | 5 | /** 6 | * Represents a Reaction on WhatsApp 7 | * @extends {Base} 8 | */ 9 | class Reaction extends Base { 10 | constructor(client, data) { 11 | super(client); 12 | 13 | if (data) this._patch(data); 14 | } 15 | 16 | _patch(data) { 17 | /** 18 | * Reaction ID 19 | * @type {object} 20 | */ 21 | this.id = data.msgKey; 22 | /** 23 | * Orphan 24 | * @type {number} 25 | */ 26 | this.orphan = data.orphan; 27 | /** 28 | * Orphan reason 29 | * @type {?string} 30 | */ 31 | this.orphanReason = data.orphanReason; 32 | /** 33 | * Unix timestamp for when the reaction was created 34 | * @type {number} 35 | */ 36 | this.timestamp = data.timestamp; 37 | /** 38 | * Reaction 39 | * @type {string} 40 | */ 41 | this.reaction = data.reactionText; 42 | /** 43 | * Read 44 | * @type {boolean} 45 | */ 46 | this.read = data.read; 47 | /** 48 | * Message ID 49 | * @type {object} 50 | */ 51 | this.msgId = data.parentMsgKey; 52 | /** 53 | * Sender ID 54 | * @type {string} 55 | */ 56 | this.senderId = data.senderUserJid; 57 | /** 58 | * ACK 59 | * @type {?number} 60 | */ 61 | this.ack = data.ack; 62 | 63 | 64 | return super._patch(data); 65 | } 66 | 67 | } 68 | 69 | module.exports = Reaction; -------------------------------------------------------------------------------- /src/structures/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Base: require('./Base'), 3 | BusinessContact: require('./BusinessContact'), 4 | Chat: require('./Chat'), 5 | ClientInfo: require('./ClientInfo'), 6 | Contact: require('./Contact'), 7 | GroupChat: require('./GroupChat'), 8 | Location: require('./Location'), 9 | Message: require('./Message'), 10 | MessageMedia: require('./MessageMedia'), 11 | PrivateChat: require('./PrivateChat'), 12 | PrivateContact: require('./PrivateContact'), 13 | GroupNotification: require('./GroupNotification'), 14 | Label: require('./Label.js'), 15 | Order: require('./Order'), 16 | Product: require('./Product'), 17 | Call: require('./Call'), 18 | Buttons: require('./Buttons'), 19 | List: require('./List'), 20 | Payment: require('./Payment'), 21 | Reaction: require('./Reaction'), 22 | Poll: require('./Poll'), 23 | }; 24 | -------------------------------------------------------------------------------- /src/util/Constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.WhatsWebURL = 'https://web.whatsapp.com/'; 4 | 5 | exports.DefaultOptions = { 6 | playwright: { 7 | headless: true, 8 | defaultViewport: null 9 | }, 10 | webVersion: '2.2347.56', 11 | webVersionCache: { 12 | type: 'local', 13 | }, 14 | authTimeoutMs: 0, 15 | qrMaxRetries: 0, 16 | takeoverOnConflict: false, 17 | takeoverTimeoutMs: 0, 18 | 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', 19 | ffmpegPath: 'ffmpeg', 20 | bypassCSP: false, 21 | proxyAuthentication: undefined 22 | }; 23 | 24 | /** 25 | * Client status 26 | * @readonly 27 | * @enum {number} 28 | */ 29 | exports.Status = { 30 | INITIALIZING: 0, 31 | AUTHENTICATING: 1, 32 | READY: 3 33 | }; 34 | 35 | /** 36 | * Events that can be emitted by the client 37 | * @readonly 38 | * @enum {string} 39 | */ 40 | exports.Events = { 41 | AUTHENTICATED: 'authenticated', 42 | AUTHENTICATION_FAILURE: 'auth_failure', 43 | READY: 'ready', 44 | CHAT_REMOVED: 'chat_removed', 45 | CHAT_ARCHIVED: 'chat_archived', 46 | MESSAGE_RECEIVED: 'message', 47 | MESSAGE_CREATE: 'message_create', 48 | MESSAGE_REVOKED_EVERYONE: 'message_revoke_everyone', 49 | MESSAGE_REVOKED_ME: 'message_revoke_me', 50 | MESSAGE_ACK: 'message_ack', 51 | MESSAGE_EDIT: 'message_edit', 52 | UNREAD_COUNT: 'unread_count', 53 | MESSAGE_REACTION: 'message_reaction', 54 | MEDIA_UPLOADED: 'media_uploaded', 55 | CONTACT_CHANGED: 'contact_changed', 56 | GROUP_JOIN: 'group_join', 57 | GROUP_LEAVE: 'group_leave', 58 | GROUP_ADMIN_CHANGED: 'group_admin_changed', 59 | GROUP_MEMBERSHIP_REQUEST: 'group_membership_request', 60 | GROUP_UPDATE: 'group_update', 61 | QR_RECEIVED: 'qr', 62 | CODE_RECEIVED: 'code', 63 | LOADING_SCREEN: 'loading_screen', 64 | DISCONNECTED: 'disconnected', 65 | STATE_CHANGED: 'change_state', 66 | BATTERY_CHANGED: 'change_battery', 67 | INCOMING_CALL: 'call', 68 | REMOTE_SESSION_SAVED: 'remote_session_saved' 69 | }; 70 | 71 | /** 72 | * Message types 73 | * @readonly 74 | * @enum {string} 75 | */ 76 | exports.MessageTypes = { 77 | TEXT: 'chat', 78 | AUDIO: 'audio', 79 | VOICE: 'ptt', 80 | IMAGE: 'image', 81 | VIDEO: 'video', 82 | DOCUMENT: 'document', 83 | STICKER: 'sticker', 84 | LOCATION: 'location', 85 | CONTACT_CARD: 'vcard', 86 | CONTACT_CARD_MULTI: 'multi_vcard', 87 | ORDER: 'order', 88 | REVOKED: 'revoked', 89 | PRODUCT: 'product', 90 | UNKNOWN: 'unknown', 91 | GROUP_INVITE: 'groups_v4_invite', 92 | LIST: 'list', 93 | LIST_RESPONSE: 'list_response', 94 | BUTTONS_RESPONSE: 'buttons_response', 95 | PAYMENT: 'payment', 96 | BROADCAST_NOTIFICATION: 'broadcast_notification', 97 | CALL_LOG: 'call_log', 98 | CIPHERTEXT: 'ciphertext', 99 | DEBUG: 'debug', 100 | E2E_NOTIFICATION: 'e2e_notification', 101 | GP2: 'gp2', 102 | GROUP_NOTIFICATION: 'group_notification', 103 | HSM: 'hsm', 104 | INTERACTIVE: 'interactive', 105 | NATIVE_FLOW: 'native_flow', 106 | NOTIFICATION: 'notification', 107 | NOTIFICATION_TEMPLATE: 'notification_template', 108 | OVERSIZED: 'oversized', 109 | PROTOCOL: 'protocol', 110 | REACTION: 'reaction', 111 | TEMPLATE_BUTTON_REPLY: 'template_button_reply', 112 | POLL_CREATION: 'poll_creation', 113 | }; 114 | 115 | /** 116 | * Group notification types 117 | * @readonly 118 | * @enum {string} 119 | */ 120 | exports.GroupNotificationTypes = { 121 | ADD: 'add', 122 | INVITE: 'invite', 123 | REMOVE: 'remove', 124 | LEAVE: 'leave', 125 | SUBJECT: 'subject', 126 | DESCRIPTION: 'description', 127 | PICTURE: 'picture', 128 | ANNOUNCE: 'announce', 129 | RESTRICT: 'restrict', 130 | }; 131 | 132 | /** 133 | * Chat types 134 | * @readonly 135 | * @enum {string} 136 | */ 137 | exports.ChatTypes = { 138 | SOLO: 'solo', 139 | GROUP: 'group', 140 | UNKNOWN: 'unknown' 141 | }; 142 | 143 | /** 144 | * WhatsApp state 145 | * @readonly 146 | * @enum {string} 147 | */ 148 | exports.WAState = { 149 | CONFLICT: 'CONFLICT', 150 | CONNECTED: 'CONNECTED', 151 | DEPRECATED_VERSION: 'DEPRECATED_VERSION', 152 | OPENING: 'OPENING', 153 | PAIRING: 'PAIRING', 154 | PROXYBLOCK: 'PROXYBLOCK', 155 | SMB_TOS_BLOCK: 'SMB_TOS_BLOCK', 156 | TIMEOUT: 'TIMEOUT', 157 | TOS_BLOCK: 'TOS_BLOCK', 158 | UNLAUNCHED: 'UNLAUNCHED', 159 | UNPAIRED: 'UNPAIRED', 160 | UNPAIRED_IDLE: 'UNPAIRED_IDLE' 161 | }; 162 | 163 | /** 164 | * Message ACK 165 | * @readonly 166 | * @enum {number} 167 | */ 168 | exports.MessageAck = { 169 | ACK_ERROR: -1, 170 | ACK_PENDING: 0, 171 | ACK_SERVER: 1, 172 | ACK_DEVICE: 2, 173 | ACK_READ: 3, 174 | ACK_PLAYED: 4, 175 | }; 176 | -------------------------------------------------------------------------------- /src/util/InterfaceController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Interface Controller 5 | */ 6 | class InterfaceController { 7 | constructor(props) { 8 | this.mPage = props.mPage; 9 | } 10 | 11 | /** 12 | * Opens the Chat Window 13 | * @param {string} chatId ID of the chat window that will be opened 14 | */ 15 | async openChatWindow(chatId) { 16 | await this.mPage.evaluate(async (chatId) => { 17 | let chatWid = window.Store.WidFactory.createWid(chatId); 18 | let chat = await window.Store.Chat.find(chatWid); 19 | await window.Store.Cmd.openChatAt(chat); 20 | }, chatId); 21 | } 22 | 23 | /** 24 | * Opens the Chat Drawer 25 | * @param {string} chatId ID of the chat drawer that will be opened 26 | */ 27 | async openChatDrawer(chatId) { 28 | await this.mPage.evaluate(async (chatId) => { 29 | let chat = await window.Store.Chat.get(chatId); 30 | await window.Store.Cmd.openDrawerMid(chat); 31 | }, chatId); 32 | } 33 | 34 | /** 35 | * Opens the Chat Search 36 | * @param {string} chatId ID of the chat search that will be opened 37 | */ 38 | async openChatSearch(chatId) { 39 | await this.mPage.evaluate(async (chatId) => { 40 | let chat = await window.Store.Chat.get(chatId); 41 | await window.Store.Cmd.chatSearch(chat); 42 | }, chatId); 43 | } 44 | 45 | /** 46 | * Opens or Scrolls the Chat Window to the position of the message 47 | * @param {string} msgId ID of the message that will be scrolled to 48 | */ 49 | async openChatWindowAt(msgId) { 50 | await this.mPage.evaluate(async (msgId) => { 51 | let msg = await window.Store.Msg.get(msgId); 52 | let chat = await window.Store.Chat.find(msg.id.remote); 53 | let searchContext = await window.Store.SearchContext(chat, msg); 54 | await window.Store.Cmd.openChatAt(chat, searchContext); 55 | }, msgId); 56 | } 57 | 58 | /** 59 | * Opens the Message Drawer 60 | * @param {string} msgId ID of the message drawer that will be opened 61 | */ 62 | async openMessageDrawer(msgId) { 63 | await this.mPage.evaluate(async (msgId) => { 64 | let msg = await window.Store.Msg.get(msgId); 65 | await window.Store.Cmd.msgInfoDrawer(msg); 66 | }, msgId); 67 | } 68 | 69 | /** 70 | * Closes the Right Drawer 71 | */ 72 | async closeRightDrawer() { 73 | await this.mPage.evaluate(async () => { 74 | await window.Store.DrawerManager.closeDrawerRight(); 75 | }); 76 | } 77 | 78 | /** 79 | * Get all Features 80 | */ 81 | async getFeatures() { 82 | return await this.mPage.evaluate(() => { 83 | if (!window.Store.Features) 84 | throw new Error( 85 | "This version of Whatsapp Web does not support features" 86 | ); 87 | return window.Store.Features.F; 88 | }); 89 | } 90 | 91 | /** 92 | * Check if Feature is enabled 93 | * @param {string} feature status to check 94 | */ 95 | async checkFeatureStatus(feature) { 96 | return await this.mPage.evaluate((feature) => { 97 | if (!window.Store.Features) 98 | throw new Error( 99 | "This version of Whatsapp Web does not support features" 100 | ); 101 | return window.Store.Features.supportsFeature(feature); 102 | }, feature); 103 | } 104 | 105 | /** 106 | * Enable Features 107 | * @param {string[]} features to be enabled 108 | */ 109 | async enableFeatures(features) { 110 | await this.mPage.evaluate((features) => { 111 | if (!window.Store.Features) 112 | throw new Error( 113 | "This version of Whatsapp Web does not support features" 114 | ); 115 | for (const feature in features) { 116 | window.Store.Features.setFeature(features[feature], true); 117 | } 118 | }, features); 119 | } 120 | 121 | /** 122 | * Disable Features 123 | * @param {string[]} features to be disabled 124 | */ 125 | async disableFeatures(features) { 126 | await this.mPage.evaluate((features) => { 127 | if (!window.Store.Features) 128 | throw new Error( 129 | "This version of Whatsapp Web does not support features" 130 | ); 131 | for (const feature in features) { 132 | window.Store.Features.setFeature(features[feature], false); 133 | } 134 | }, features); 135 | } 136 | } 137 | 138 | module.exports = InterfaceController; 139 | -------------------------------------------------------------------------------- /src/util/Util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * MywaJS 2023 3 | * re-developed wwebjs 4 | * using with playwright & wajs 5 | * contact: 6 | * wa: 085157489446 7 | * ig: amirul.dev 8 | */ 9 | 'use strict'; 10 | const path = require('path'); 11 | const Crypto = require('crypto'); 12 | const { tmpdir } = require('os'); 13 | const ffmpeg = require('fluent-ffmpeg'); 14 | const webp = require('node-webpmux'); 15 | const fs = require('fs').promises; 16 | const sharp = require('sharp') 17 | const { Readable } = require('stream') 18 | const Fs = require('fs') 19 | const axios = require('axios') 20 | const BodyForm = require('form-data') 21 | const fileType = require('file-type') 22 | const mimes = require('mime-types') 23 | 24 | const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k); 25 | /** 26 | * Utility methods 27 | */ 28 | class Util { 29 | constructor() { 30 | throw new Error(`The ${this.constructor.name} class may not be instantiated.`); 31 | } 32 | 33 | static sleep(ms) { 34 | return new Promise(resolve => setTimeout(resolve, ms)); 35 | } 36 | 37 | static getRandom(ext = "", length = "10") { 38 | var result = "" 39 | var character = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890" 40 | var characterLength = character.length 41 | for (var i = 0; i < length; i++) { 42 | result += character.charAt(Math.floor(Math.random() * characterLength)) 43 | } 44 | 45 | return `${result}${ext ? `.${ext}` : ""}` 46 | } 47 | 48 | static bufferToBase64(buffer) { 49 | if (!Buffer.isBuffer(buffer)) throw new Error("Buffer Not Detected") 50 | 51 | var buf = new Buffer(buffer) 52 | return buf.toString('base64') 53 | } 54 | 55 | static formatSize(bytes) { 56 | if (bytes >= 1000000000) { 57 | bytes = (bytes / 1000000000).toFixed(2) + " GB"; 58 | } else if (bytes >= 1000000) { 59 | bytes = (bytes / 1000000).toFixed(2) + " MB"; 60 | } else if (bytes >= 1000) { 61 | bytes = (bytes / 1000).toFixed(2) + " KB"; 62 | } else if (bytes > 1) { 63 | bytes = bytes + " bytes"; 64 | } else if (bytes == 1) { 65 | bytes = bytes + " byte"; 66 | } else { 67 | bytes = "0 bytes"; 68 | } 69 | return bytes; 70 | } 71 | 72 | static isBase64(string) { 73 | const regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/ 74 | return regex.test(string) 75 | } 76 | 77 | static isUrl(url) { 78 | return url.match(new RegExp(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/, 'gi')) 79 | } 80 | 81 | static generateHash(length) { 82 | var result = ""; 83 | var characters = 84 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 85 | var charactersLength = characters.length; 86 | for (var i = 0; i < length; i++) { 87 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 88 | } 89 | return result; 90 | } 91 | 92 | static base64ToBuffer(base) { 93 | return Buffer.from(base, 'base64') 94 | } 95 | 96 | static generateHash(length) { 97 | var result = ''; 98 | var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 99 | var charactersLength = characters.length; 100 | for (var i = 0; i < length; i++) { 101 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 102 | } 103 | return result; 104 | } 105 | 106 | /** 107 | * Sets default properties on an object that aren't already specified. 108 | * @param {Object} def Default properties 109 | * @param {Object} given Object to assign defaults to 110 | * @returns {Object} 111 | * @private 112 | */ 113 | static mergeDefault(def, given) { 114 | if (!given) return def; 115 | for (const key in def) { 116 | if (!has(given, key) || given[key] === undefined) { 117 | given[key] = def[key]; 118 | } else if (given[key] === Object(given[key])) { 119 | given[key] = Util.mergeDefault(def[key], given[key]); 120 | } 121 | } 122 | 123 | return given; 124 | } 125 | 126 | /** 127 | * Formats a image to webp 128 | * @param {MessageMedia} media 129 | * 130 | * @returns {Promise} media in webp format 131 | */ 132 | static async formatImageToWebpSticker(media, mPage) { 133 | if (!media.mimetype.includes('image')) 134 | throw new Error('media is not a image'); 135 | 136 | if (media.mimetype.includes('webp')) { 137 | return media; 138 | } 139 | 140 | return mPage.evaluate((media) => { 141 | return window.WWebJS.toStickerData(media); 142 | }, media); 143 | } 144 | 145 | /** 146 | * Formats a video to webp 147 | * @param {MessageMedia} media 148 | * 149 | * @returns {Promise} media in webp format 150 | */ 151 | static async formatVideoToWebpSticker(media) { 152 | if (!media.mimetype.includes('video')) 153 | throw new Error('media is not a video'); 154 | 155 | const videoType = media.mimetype.split('/')[1]; 156 | 157 | const tempFile = path.join( 158 | tmpdir(), 159 | `${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp` 160 | ); 161 | 162 | const stream = new Readable(); 163 | const buffer = Buffer.from( 164 | media.data.replace(`data:${media.mimetype};base64,`, ''), 165 | 'base64' 166 | ); 167 | stream.push(buffer); 168 | stream.push(null); 169 | 170 | await new Promise((resolve, reject) => { 171 | ffmpeg(stream) 172 | .inputFormat(videoType) 173 | .on('error', reject) 174 | .on('end', () => resolve(true)) 175 | .addOutputOptions([ 176 | '-vcodec', 177 | 'libwebp', 178 | '-vf', 179 | // eslint-disable-next-line no-useless-escape 180 | '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', 181 | '-loop', 182 | '0', 183 | '-ss', 184 | '00:00:00.0', 185 | '-t', 186 | '00:00:05.0', 187 | '-preset', 188 | 'default', 189 | '-an', 190 | '-vsync', 191 | '0', 192 | '-s', 193 | '512:512', 194 | ]) 195 | .toFormat('webp') 196 | .save(tempFile); 197 | }); 198 | 199 | const data = await fs.readFile(tempFile, 'base64'); 200 | await fs.unlink(tempFile); 201 | 202 | return { 203 | mimetype: 'image/webp', 204 | data: data, 205 | filename: media.filename, 206 | }; 207 | } 208 | 209 | /** 210 | * Sticker metadata. 211 | * @typedef {Object} StickerMetadata 212 | * @property {string} [name] 213 | * @property {string} [author] 214 | * @property {string[]} [categories] 215 | */ 216 | 217 | /** 218 | * Formats a media to webp 219 | * @param {MessageMedia} media 220 | * @param {StickerMetadata} metadata 221 | * 222 | * @returns {Promise} media in webp format 223 | */ 224 | static async formatToWebpSticker(media, metadata, playPage) { 225 | let webpMedia; 226 | 227 | if (media.mimetype.includes("webp")) 228 | webpMedia = { 229 | mimetype: "image/webp", 230 | data: media.data, 231 | filename: undefined, 232 | }; 233 | else if (media.mimetype.includes("image")) 234 | webpMedia = await this.formatImageToWebpSticker(media, playPage); 235 | else if (media.mimetype.includes("video")) 236 | webpMedia = await this.formatVideoToWebpSticker(media); 237 | else throw new Error("Invalid media format"); 238 | 239 | if (typeof metadata === "object" && metadata !== null) { 240 | const img = new webp.Image(); 241 | const hash = this.generateHash(32); 242 | const json = { 243 | "sticker-pack-id": metadata.packId ? metadata.packId : hash, 244 | "sticker-pack-name": metadata.packName ? 245 | metadata.packName : 246 | "MywaJS", 247 | "sticker-pack-publisher": metadata.packPublish ? 248 | metadata.packPublish : 249 | "Amirul Dev", 250 | "sticker-pack-publisher-email": metadata.packEmail ? 251 | metadata.packEmail : 252 | "", 253 | "sticker-pack-publisher-website": metadata.packWebsite ? 254 | metadata.packWebsite : 255 | "https://instagram.com/amirul.dev", 256 | "android-app-store-link": metadata.androidApp ? 257 | metadata.androidApp : 258 | "", 259 | "ios-app-store-link": metadata.iOSApp ? metadata.iOSApp : "", 260 | emojis: metadata.categories ? metadata.categories : [], 261 | "is-avatar-sticker": metadata.isAvatar ? metadata.isAvatar : 0, 262 | }; 263 | let exifAttr = Buffer.from([ 264 | 0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 265 | 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 266 | ]); 267 | let jsonBuffer = Buffer.from(JSON.stringify(json), "utf8"); 268 | let exif = Buffer.concat([exifAttr, jsonBuffer]); 269 | exif.writeUIntLE(jsonBuffer.length, 14, 4); 270 | await img.load(Buffer.from(webpMedia.data, "base64")); 271 | img.exif = exif; 272 | webpMedia.data = (await img.save(null)).toString("base64"); 273 | } 274 | 275 | return webpMedia; 276 | } 277 | 278 | /** 279 | * Configure ffmpeg path 280 | * @param {string} path 281 | */ 282 | static setFfmpegPath(path) { 283 | ffmpeg.setFfmpegPath(path); 284 | } 285 | 286 | /* fetch buffer */ 287 | static fetchBuffer(string, options = {}) { 288 | return new Promise(async (resolve, reject) => { 289 | try { 290 | if (/^https?:\/\//i.test(string)) { 291 | let data = await axios.get(string, { 292 | headers: { 293 | ...(!!options.headers ? options.headers : {}), 294 | }, 295 | responseType: "arraybuffer", 296 | ...options, 297 | }) 298 | let buffer = await data ?.data 299 | let name = /filename/i.test(data.headers ?.get("content-disposition")) ? data.headers ?.get("content-disposition") ?.match(/filename=(.*)/) ?.[1] ?.replace(/["';]/g, '') : '' 300 | let mime = mimes.lookup(name) || data.headers.get("content-type") || (await fileType.fromBuffer(buffer)) ?.mime 301 | resolve({ 302 | data: buffer, 303 | size: Buffer.byteLength(buffer), 304 | sizeH: this.formatSize(Buffer.byteLength(buffer)), 305 | name, 306 | mime, 307 | ext: mimes.extension(mime) 308 | }); 309 | } else if (/^data:.*?\/.*?;base64,/i.test(string)) { 310 | let data = Buffer.from(string.split`,`[1], "base64") 311 | let size = Buffer.byteLength(data) 312 | resolve({ 313 | data, 314 | size, 315 | sizeH: this.formatSize(size), 316 | ...((await fileType.fromBuffer(data)) || { 317 | mime: "application/octet-stream", 318 | ext: ".bin" 319 | }) 320 | }); 321 | } else if (Fs.existsSync(string) && Fs.statSync(string).isFile()) { 322 | let data = Fs.readFileSync(string) 323 | let size = Buffer.byteLength(data) 324 | resolve({ 325 | data, 326 | size, 327 | sizeH: this.formatSize(size), 328 | ...((await fileType.fromBuffer(data)) || { 329 | mime: "application/octet-stream", 330 | ext: ".bin" 331 | }) 332 | }); 333 | } else if (Buffer.isBuffer(string)) { 334 | let size = Buffer ?.byteLength(string) || 0 335 | resolve({ 336 | data: string, 337 | size, 338 | sizeH: this.formatSize(size), 339 | ...((await fileType.fromBuffer(string)) || { 340 | mime: "application/octet-stream", 341 | ext: ".bin" 342 | }) 343 | }); 344 | } else if (/^[a-zA-Z0-9+/]={0,2}$/i.test(string)) { 345 | let data = Buffer.from(string, "base64") 346 | let size = Buffer.byteLength(data) 347 | resolve({ 348 | data, 349 | size, 350 | sizeH: this.formatSize(size), 351 | ...((await fileType.fromBuffer(data)) || { 352 | mime: "application/octet-stream", 353 | ext: ".bin" 354 | }) 355 | }); 356 | } else { 357 | let buffer = Buffer.alloc(20) 358 | let size = Buffer.byteLength(buffer) 359 | resolve({ 360 | data: buffer, 361 | size, 362 | sizeH: this.formatSize(size), 363 | ...((await fileType.fromBuffer(buffer)) || { 364 | mime: "application/octet-stream", 365 | ext: ".bin" 366 | }) 367 | }); 368 | } 369 | } catch (e) { 370 | reject(new Error(e ?.message || e)) 371 | } 372 | }); 373 | } 374 | 375 | /* get file */ 376 | static async getFile(PATH, save, options = {}) { 377 | try { 378 | options = !!options.headers ? options.headers : {} 379 | let filename = null; 380 | let data = (await this.fetchBuffer(PATH, { 381 | headers: { 382 | referer: 'https://y2mate.com' 383 | } 384 | })) 385 | 386 | if (data ?.data && save) { 387 | filename = `../../temp/${Date.now()}.${data.ext}` 388 | Fs.promises.writeFile(filename, data ?.data); 389 | } 390 | return { 391 | filename: data ?.name ? data.name : filename, 392 | ...data 393 | }; 394 | } catch (e) { 395 | throw e 396 | } 397 | } 398 | 399 | /* upload media */ 400 | static upload(buffer, exts) { 401 | return new Promise(async (resolve, reject) => { 402 | const { ext, data: buffers } = await this.getFile(buffer) 403 | const form = new BodyForm(); 404 | form.append("files[]", buffers, this.getRandom(exts || ext)) 405 | await axios({ 406 | url: "https://pomf.lain.la/upload.php", 407 | method: "POST", 408 | headers: { 409 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36", 410 | ...form.getHeaders() 411 | }, 412 | data: form 413 | }).then((data) => { 414 | resolve(data.data.files[0]) 415 | }).catch((err) => resolve(err)) 416 | }) 417 | } 418 | 419 | /* resize image */ 420 | static async resizeImage(buffer, height) { 421 | buffer = (await this.getFile(buffer)).data 422 | /** 423 | * @param {Sharp} img 424 | * @param {number} maxSize 425 | * @return {Promise} 426 | */ 427 | const resizeByMax = async (img, maxSize) => { 428 | const metadata = await img.metadata(); 429 | const outputRatio = maxSize / Math.max(metadata.height, metadata.width); 430 | return img.resize(Math.floor(metadata.width * outputRatio), Math.floor(metadata.height * outputRatio)); 431 | }; 432 | 433 | const img = await sharp(buffer) 434 | 435 | return (await resizeByMax(img, height)).toFormat('jpg').toBuffer() 436 | } 437 | 438 | } 439 | 440 | module.exports = Util; -------------------------------------------------------------------------------- /src/webCache/LocalWebCache.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | const { WebCache, VersionResolveError } = require('./WebCache'); 5 | 6 | /** 7 | * LocalWebCache - Fetches a WhatsApp Web version from a local file store 8 | * @param {object} options - options 9 | * @param {string} options.path - Path to the directory where cached versions are saved, default is: "./.wwebjs_cache/" 10 | * @param {boolean} options.strict - If true, will throw an error if the requested version can't be fetched. If false, will resolve to the latest version. 11 | */ 12 | class LocalWebCache extends WebCache { 13 | constructor(options = {}) { 14 | super(); 15 | 16 | this.path = options.path || './.mywa_cache/'; 17 | this.strict = options.strict || false; 18 | } 19 | 20 | async resolve(version) { 21 | const filePath = path.join(this.path, `${version}.html`); 22 | 23 | try { 24 | return fs.readFileSync(filePath, 'utf-8'); 25 | } 26 | catch (err) { 27 | if (this.strict) throw new VersionResolveError(`Couldn't load version ${version} from the cache`); 28 | return null; 29 | } 30 | } 31 | 32 | async persist(indexHtml) { 33 | // extract version from index (e.g. manifest-2.2206.9.json -> 2.2206.9) 34 | const version = indexHtml.match(/manifest-([\d\\.]+)\.json/)[1]; 35 | if(!version) return; 36 | 37 | const filePath = path.join(this.path, `${version}.html`); 38 | fs.mkdirSync(this.path, { recursive: true }); 39 | fs.writeFileSync(filePath, indexHtml); 40 | } 41 | } 42 | 43 | module.exports = LocalWebCache; -------------------------------------------------------------------------------- /src/webCache/RemoteWebCache.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const { WebCache, VersionResolveError } = require('./WebCache'); 3 | 4 | /** 5 | * RemoteWebCache - Fetches a WhatsApp Web version index from a remote server 6 | * @param {object} options - options 7 | * @param {string} options.remotePath - Endpoint that should be used to fetch the version index. Use {version} as a placeholder for the version number. 8 | * @param {boolean} options.strict - If true, will throw an error if the requested version can't be fetched. If false, will resolve to the latest version. Defaults to false. 9 | */ 10 | class RemoteWebCache extends WebCache { 11 | constructor(options = {}) { 12 | super(); 13 | 14 | if (!options.remotePath) throw new Error('webVersionCache.remotePath is required when using the remote cache'); 15 | this.remotePath = options.remotePath; 16 | this.strict = options.strict || false; 17 | } 18 | 19 | async resolve(version) { 20 | const remotePath = this.remotePath.replace('{version}', version); 21 | 22 | try { 23 | const cachedRes = await fetch(remotePath); 24 | if (cachedRes.ok) { 25 | return cachedRes.text(); 26 | } 27 | } catch (err) { 28 | console.error(`Error fetching version ${version} from remote`, err); 29 | } 30 | 31 | if (this.strict) throw new VersionResolveError(`Couldn't load version ${version} from the archive`); 32 | return null; 33 | } 34 | 35 | async persist() { 36 | // Nothing to do here 37 | } 38 | } 39 | 40 | module.exports = RemoteWebCache; -------------------------------------------------------------------------------- /src/webCache/WebCache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default implementation of a web version cache that does nothing. 3 | */ 4 | class WebCache { 5 | async resolve() { return null; } 6 | async persist() { } 7 | } 8 | 9 | class VersionResolveError extends Error { } 10 | 11 | module.exports = { 12 | WebCache, 13 | VersionResolveError 14 | }; -------------------------------------------------------------------------------- /src/webCache/WebCacheFactory.js: -------------------------------------------------------------------------------- 1 | const RemoteWebCache = require('./RemoteWebCache'); 2 | const LocalWebCache = require('./LocalWebCache'); 3 | const { WebCache } = require('./WebCache'); 4 | 5 | const createWebCache = (type, options) => { 6 | switch (type) { 7 | case 'remote': 8 | return new RemoteWebCache(options); 9 | case 'local': 10 | return new LocalWebCache(options); 11 | case 'none': 12 | return new WebCache(); 13 | default: 14 | throw new Error(`Invalid WebCache type ${type}`); 15 | } 16 | }; 17 | 18 | module.exports = { 19 | createWebCache, 20 | }; -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | ## Running tests 2 | 3 | These tests require an authenticated WhatsApp Web session, as well as an additional phone that you can send messages to. 4 | 5 | This can be configured using the following environment variables: 6 | - `WWEBJS_TEST_SESSION`: A JSON-formatted string with legacy auth session details. Must include `WABrowserId`, `WASecretBundle`, `WAToken1` and `WAToken2`. 7 | - `WWEBJS_TEST_SESSION_PATH`: Path to a JSON file that contains the legacy auth session details. Must include `WABrowserId`, `WASecretBundle`, `WAToken1` and `WAToken2`. 8 | - `WWEBJS_TEST_CLIENT_ID`: `clientId` to use for local file based authentication. 9 | - `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. 10 | 11 | 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. 12 | 13 | ### Multidevice 14 | 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`. -------------------------------------------------------------------------------- /tests/helper.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { Client, LegacySessionAuth, LocalAuth } = require('..'); 3 | 4 | require('dotenv').config(); 5 | 6 | const remoteId = process.env.WWEBJS_TEST_REMOTE_ID; 7 | if(!remoteId) throw new Error('The WWEBJS_TEST_REMOTE_ID environment variable has not been set.'); 8 | 9 | function isUsingLegacySession() { 10 | return Boolean(process.env.WWEBJS_TEST_SESSION || process.env.WWEBJS_TEST_SESSION_PATH); 11 | } 12 | 13 | function isMD() { 14 | return Boolean(process.env.WWEBJS_TEST_MD); 15 | } 16 | 17 | if(isUsingLegacySession() && isMD()) throw 'Cannot use legacy sessions with WWEBJS_TEST_MD=true'; 18 | 19 | function getSessionFromEnv() { 20 | if (!isUsingLegacySession()) return null; 21 | 22 | const envSession = process.env.WWEBJS_TEST_SESSION; 23 | if(envSession) return JSON.parse(envSession); 24 | 25 | const envSessionPath = process.env.WWEBJS_TEST_SESSION_PATH; 26 | if(envSessionPath) { 27 | const absPath = path.resolve(process.cwd(), envSessionPath); 28 | return require(absPath); 29 | } 30 | } 31 | 32 | function createClient({authenticated, options: additionalOpts}={}) { 33 | const options = {}; 34 | 35 | if(authenticated) { 36 | const legacySession = getSessionFromEnv(); 37 | if(legacySession) { 38 | options.authStrategy = new LegacySessionAuth({ 39 | session: legacySession 40 | }); 41 | } else { 42 | const clientId = process.env.WWEBJS_TEST_CLIENT_ID; 43 | if(!clientId) throw new Error('No session found in environment.'); 44 | options.authStrategy = new LocalAuth({ 45 | clientId 46 | }); 47 | } 48 | } 49 | 50 | const allOpts = {...options, ...(additionalOpts || {})}; 51 | return new Client(allOpts); 52 | } 53 | 54 | function sleep(ms) { 55 | return new Promise(resolve => setTimeout(resolve, ms)); 56 | } 57 | 58 | module.exports = { 59 | sleep, 60 | createClient, 61 | isUsingLegacySession, 62 | isMD, 63 | remoteId, 64 | }; -------------------------------------------------------------------------------- /tests/structures/chat.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | 3 | const helper = require('../helper'); 4 | const Message = require('../../src/structures/Message'); 5 | const { MessageTypes } = require('../../src/util/Constants'); 6 | const { Contact } = require('../../src/structures'); 7 | 8 | const remoteId = helper.remoteId; 9 | 10 | describe('Chat', function () { 11 | let client; 12 | let chat; 13 | 14 | before(async function() { 15 | this.timeout(35000); 16 | client = helper.createClient({ authenticated: true }); 17 | await client.initialize(); 18 | chat = await client.getChatById(remoteId); 19 | }); 20 | 21 | after(async function () { 22 | await client.destroy(); 23 | }); 24 | 25 | it('can send a message to a chat', async function () { 26 | const msg = await chat.sendMessage('hello world'); 27 | expect(msg).to.be.instanceOf(Message); 28 | expect(msg.type).to.equal(MessageTypes.TEXT); 29 | expect(msg.fromMe).to.equal(true); 30 | expect(msg.body).to.equal('hello world'); 31 | expect(msg.to).to.equal(remoteId); 32 | }); 33 | 34 | it('can fetch messages sent in a chat', async function () { 35 | await helper.sleep(1000); 36 | const msg = await chat.sendMessage('another message'); 37 | await helper.sleep(500); 38 | 39 | const messages = await chat.fetchMessages(); 40 | expect(messages.length).to.be.greaterThanOrEqual(2); 41 | 42 | const fetchedMsg = messages[messages.length-1]; 43 | expect(fetchedMsg).to.be.instanceOf(Message); 44 | expect(fetchedMsg.type).to.equal(MessageTypes.TEXT); 45 | expect(fetchedMsg.id._serialized).to.equal(msg.id._serialized); 46 | expect(fetchedMsg.body).to.equal(msg.body); 47 | }); 48 | 49 | it('can use a limit when fetching messages sent in a chat', async function () { 50 | await helper.sleep(1000); 51 | const msg = await chat.sendMessage('yet another message'); 52 | await helper.sleep(500); 53 | 54 | const messages = await chat.fetchMessages({limit: 1}); 55 | expect(messages).to.have.lengthOf(1); 56 | 57 | const fetchedMsg = messages[0]; 58 | expect(fetchedMsg).to.be.instanceOf(Message); 59 | expect(fetchedMsg.type).to.equal(MessageTypes.TEXT); 60 | expect(fetchedMsg.id._serialized).to.equal(msg.id._serialized); 61 | expect(fetchedMsg.body).to.equal(msg.body); 62 | }); 63 | 64 | it('can use fromMe=true when fetching messages sent in a chat to get only bot messages', async function () { 65 | const messages = await chat.fetchMessages({fromMe: true}); 66 | expect(messages).to.have.lengthOf(2); 67 | }); 68 | 69 | it('can use fromMe=false when fetching messages sent in a chat to get only non bot messages', async function () { 70 | const messages = await chat.fetchMessages({fromMe: false}); 71 | expect(messages).to.have.lengthOf(0); 72 | }); 73 | 74 | it('can get the related contact', async function () { 75 | const contact = await chat.getContact(); 76 | expect(contact).to.be.instanceOf(Contact); 77 | expect(contact.id._serialized).to.equal(chat.id._serialized); 78 | }); 79 | 80 | describe('Seen', function () { 81 | it('can mark a chat as unread', async function () { 82 | await chat.markUnread(); 83 | await helper.sleep(500); 84 | 85 | // refresh chat 86 | chat = await client.getChatById(remoteId); 87 | expect(chat.unreadCount).to.equal(-1); 88 | }); 89 | 90 | it('can mark a chat as seen', async function () { 91 | const res = await chat.sendSeen(); 92 | expect(res).to.equal(true); 93 | 94 | await helper.sleep(1000); 95 | 96 | // refresh chat 97 | chat = await client.getChatById(remoteId); 98 | expect(chat.unreadCount).to.equal(0); 99 | }); 100 | }); 101 | 102 | describe('Archiving', function (){ 103 | it('can archive a chat', async function () { 104 | const res = await chat.archive(); 105 | expect(res).to.equal(true); 106 | 107 | await helper.sleep(1000); 108 | 109 | // refresh chat 110 | chat = await client.getChatById(remoteId); 111 | expect(chat.archived).to.equal(true); 112 | }); 113 | 114 | it('can unarchive a chat', async function () { 115 | const res = await chat.unarchive(); 116 | expect(res).to.equal(false); 117 | 118 | await helper.sleep(1000); 119 | 120 | // refresh chat 121 | chat = await client.getChatById(remoteId); 122 | expect(chat.archived).to.equal(false); 123 | }); 124 | }); 125 | 126 | describe('Pinning', function () { 127 | it('can pin a chat', async function () { 128 | const res = await chat.pin(); 129 | expect(res).to.equal(true); 130 | 131 | await helper.sleep(1000); 132 | 133 | // refresh chat 134 | chat = await client.getChatById(remoteId); 135 | expect(chat.pinned).to.equal(true); 136 | }); 137 | 138 | it('can unpin a chat', async function () { 139 | const res = await chat.unpin(); 140 | expect(res).to.equal(false); 141 | await helper.sleep(1000); 142 | 143 | // refresh chat 144 | chat = await client.getChatById(remoteId); 145 | expect(chat.pinned).to.equal(false); 146 | }); 147 | }); 148 | 149 | describe('Muting', function () { 150 | it('can mute a chat forever', async function() { 151 | await chat.mute(); 152 | 153 | await helper.sleep(1000); 154 | 155 | // refresh chat 156 | chat = await client.getChatById(remoteId); 157 | expect(chat.isMuted).to.equal(true); 158 | expect(chat.muteExpiration).to.equal(-1); 159 | }); 160 | 161 | it('can mute a chat until a specific date', async function() { 162 | const unmuteDate = new Date(new Date().getTime() + (1000*60*60)); 163 | await chat.mute(unmuteDate); 164 | 165 | await helper.sleep(1000); 166 | 167 | // refresh chat 168 | chat = await client.getChatById(remoteId); 169 | expect(chat.isMuted).to.equal(true); 170 | expect(chat.muteExpiration).to.equal( 171 | Math.round(unmuteDate.getTime() / 1000) 172 | ); 173 | }); 174 | 175 | it('can unmute a chat', async function () { 176 | await chat.unmute(); 177 | await helper.sleep(500); 178 | 179 | // refresh chat 180 | chat = await client.getChatById(remoteId); 181 | expect(chat.isMuted).to.equal(false); 182 | expect(chat.muteExpiration).to.equal(0); 183 | }); 184 | }); 185 | 186 | // eslint-disable-next-line mocha/no-skipped-tests 187 | describe.skip('Destructive operations', function () { 188 | it('can clear all messages from chat', async function () { 189 | const res = await chat.clearMessages(); 190 | expect(res).to.equal(true); 191 | 192 | await helper.sleep(3000); 193 | 194 | const msgs = await chat.fetchMessages(); 195 | expect(msgs).to.have.lengthOf(0); 196 | }); 197 | 198 | it('can delete a chat', async function () { 199 | const res = await chat.delete(); 200 | expect(res).to.equal(true); 201 | }); 202 | }); 203 | }); -------------------------------------------------------------------------------- /tests/structures/group.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const helper = require('../helper'); 3 | 4 | const remoteId = helper.remoteId; 5 | 6 | describe('Group', function() { 7 | let client; 8 | let group; 9 | 10 | before(async function() { 11 | this.timeout(35000); 12 | client = helper.createClient({ 13 | authenticated: true, 14 | }); 15 | await client.initialize(); 16 | 17 | const createRes = await client.createGroup('My Awesome Group', [remoteId]); 18 | expect(createRes.gid).to.exist; 19 | await helper.sleep(500); 20 | group = await client.getChatById(createRes.gid._serialized); 21 | expect(group).to.exist; 22 | }); 23 | 24 | beforeEach(async function () { 25 | await helper.sleep(500); 26 | }); 27 | 28 | describe('Settings', function () { 29 | it('can change the group subject', async function () { 30 | expect(group.name).to.equal('My Awesome Group'); 31 | const res = await group.setSubject('My Amazing Group'); 32 | expect(res).to.equal(true); 33 | 34 | await helper.sleep(1000); 35 | 36 | // reload 37 | group = await client.getChatById(group.id._serialized); 38 | expect(group.name).to.equal('My Amazing Group'); 39 | }); 40 | 41 | it('can change the group description', async function () { 42 | expect(group.description).to.equal(undefined); 43 | const res = await group.setDescription('some description'); 44 | expect(res).to.equal(true); 45 | expect(group.description).to.equal('some description'); 46 | 47 | await helper.sleep(1000); 48 | 49 | // reload 50 | group = await client.getChatById(group.id._serialized); 51 | expect(group.description).to.equal('some description'); 52 | }); 53 | 54 | it('can set only admins able to send messages', async function () { 55 | expect(group.groupMetadata.announce).to.equal(false); 56 | const res = await group.setMessagesAdminsOnly(); 57 | expect(res).to.equal(true); 58 | expect(group.groupMetadata.announce).to.equal(true); 59 | 60 | await helper.sleep(1000); 61 | 62 | // reload 63 | group = await client.getChatById(group.id._serialized); 64 | expect(group.groupMetadata.announce).to.equal(true); 65 | }); 66 | 67 | it('can set all participants able to send messages', async function () { 68 | expect(group.groupMetadata.announce).to.equal(true); 69 | const res = await group.setMessagesAdminsOnly(false); 70 | expect(res).to.equal(true); 71 | expect(group.groupMetadata.announce).to.equal(false); 72 | 73 | await helper.sleep(1000); 74 | 75 | // reload 76 | group = await client.getChatById(group.id._serialized); 77 | expect(group.groupMetadata.announce).to.equal(false); 78 | }); 79 | 80 | it('can set only admins able to set group info', async function () { 81 | expect(group.groupMetadata.restrict).to.equal(false); 82 | const res = await group.setInfoAdminsOnly(); 83 | expect(res).to.equal(true); 84 | expect(group.groupMetadata.restrict).to.equal(true); 85 | 86 | await helper.sleep(1000); 87 | 88 | // reload 89 | group = await client.getChatById(group.id._serialized); 90 | expect(group.groupMetadata.restrict).to.equal(true); 91 | }); 92 | 93 | it('can set all participants able to set group info', async function () { 94 | expect(group.groupMetadata.restrict).to.equal(true); 95 | const res = await group.setInfoAdminsOnly(false); 96 | expect(res).to.equal(true); 97 | expect(group.groupMetadata.restrict).to.equal(false); 98 | 99 | await helper.sleep(1000); 100 | 101 | // reload 102 | group = await client.getChatById(group.id._serialized); 103 | expect(group.groupMetadata.restrict).to.equal(false); 104 | }); 105 | }); 106 | 107 | describe('Invites', function () { 108 | it('can get the invite code', async function () { 109 | const code = await group.getInviteCode(); 110 | expect(typeof code).to.equal('string'); 111 | }); 112 | 113 | it('can get invite info', async function () { 114 | const code = await group.getInviteCode(); 115 | const info = await client.getInviteInfo(code); 116 | expect(info.id._serialized).to.equal(group.id._serialized); 117 | expect(info.participants.length).to.equal(2); 118 | }); 119 | 120 | it('can revoke the invite code', async function () { 121 | const code = await group.getInviteCode(); 122 | const newCode = await group.revokeInvite(); 123 | expect(typeof newCode).to.equal('string'); 124 | expect(newCode).to.not.equal(code); 125 | }); 126 | }); 127 | 128 | describe('Participants', function () { 129 | it('can promote a user to admin', async function () { 130 | let participant = group.participants.find(p => p.id._serialized === remoteId); 131 | expect(participant).to.exist; 132 | expect(participant.isAdmin).to.equal(false); 133 | 134 | const res = await group.promoteParticipants([remoteId]); 135 | expect(res.status).to.be.greaterThanOrEqual(200); 136 | 137 | await helper.sleep(1000); 138 | 139 | // reload and check 140 | group = await client.getChatById(group.id._serialized); 141 | participant = group.participants.find(p => p.id._serialized=== remoteId); 142 | expect(participant).to.exist; 143 | expect(participant.isAdmin).to.equal(true); 144 | }); 145 | 146 | it('can demote a user', async function () { 147 | let participant = group.participants.find(p => p.id._serialized=== remoteId); 148 | expect(participant).to.exist; 149 | expect(participant.isAdmin).to.equal(true); 150 | 151 | const res = await group.demoteParticipants([remoteId]); 152 | expect(res.status).to.be.greaterThanOrEqual(200); 153 | 154 | await helper.sleep(1000); 155 | 156 | // reload and check 157 | group = await client.getChatById(group.id._serialized); 158 | participant = group.participants.find(p => p.id._serialized=== remoteId); 159 | expect(participant).to.exist; 160 | expect(participant.isAdmin).to.equal(false); 161 | }); 162 | 163 | it('can remove a user from the group', async function () { 164 | let participant = group.participants.find(p => p.id._serialized=== remoteId); 165 | expect(participant).to.exist; 166 | 167 | const res = await group.removeParticipants([remoteId]); 168 | expect(res.status).to.be.greaterThanOrEqual(200); 169 | 170 | await helper.sleep(1000); 171 | 172 | // reload and check 173 | group = await client.getChatById(group.id._serialized); 174 | participant = group.participants.find(p => p.id._serialized=== remoteId); 175 | expect(participant).to.not.exist; 176 | }); 177 | 178 | it('can add back a user to the group', async function () { 179 | let participant = group.participants.find(p => p.id._serialized=== remoteId); 180 | expect(participant).to.not.exist; 181 | 182 | const res = await group.addParticipants([remoteId]); 183 | expect(res.status).to.be.greaterThanOrEqual(200); 184 | 185 | await helper.sleep(1000); 186 | 187 | // reload and check 188 | group = await client.getChatById(group.id._serialized); 189 | participant = group.participants.find(p => p.id._serialized=== remoteId); 190 | expect(participant).to.exist; 191 | }); 192 | }); 193 | 194 | describe('Leave / re-join', function () { 195 | let code; 196 | before(async function () { 197 | code = await group.getInviteCode(); 198 | }); 199 | 200 | it('can leave the group', async function () { 201 | expect(group.isReadOnly).to.equal(false); 202 | await group.leave(); 203 | 204 | await helper.sleep(1000); 205 | 206 | // reload and check 207 | group = await client.getChatById(group.id._serialized); 208 | expect(group.isReadOnly).to.equal(true); 209 | }); 210 | 211 | it('can join a group via invite code', async function () { 212 | const chatId = await client.acceptInvite(code); 213 | expect(chatId).to.equal(group.id._serialized); 214 | 215 | await helper.sleep(1000); 216 | 217 | // reload and check 218 | group = await client.getChatById(group.id._serialized); 219 | expect(group.isReadOnly).to.equal(false); 220 | }); 221 | }); 222 | 223 | after(async function () { 224 | await client.destroy(); 225 | }); 226 | 227 | }); 228 | -------------------------------------------------------------------------------- /tests/structures/message.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const sinon = require('sinon'); 3 | 4 | const helper = require('../helper'); 5 | const { Contact, Chat } = require('../../src/structures'); 6 | 7 | const remoteId = helper.remoteId; 8 | 9 | describe('Message', function () { 10 | let client; 11 | let chat; 12 | let message; 13 | 14 | before(async function() { 15 | this.timeout(35000); 16 | client = helper.createClient({ authenticated: true }); 17 | await client.initialize(); 18 | 19 | chat = await client.getChatById(remoteId); 20 | message = await chat.sendMessage('this is only a test'); 21 | 22 | // wait for message to be sent 23 | await helper.sleep(1000); 24 | }); 25 | 26 | after(async function () { 27 | await client.destroy(); 28 | }); 29 | 30 | it('can get the related chat', async function () { 31 | const chat = await message.getChat(); 32 | expect(chat).to.be.instanceOf(Chat); 33 | expect(chat.id._serialized).to.equal(remoteId); 34 | }); 35 | 36 | it('can get the related contact', async function () { 37 | const contact = await message.getContact(); 38 | expect(contact).to.be.instanceOf(Contact); 39 | expect(contact.id._serialized).to.equal(client.info.wid._serialized); 40 | }); 41 | 42 | it('can get message info', async function () { 43 | const info = await message.getInfo(); 44 | expect(typeof info).to.equal('object'); 45 | expect(Array.isArray(info.played)).to.equal(true); 46 | expect(Array.isArray(info.read)).to.equal(true); 47 | expect(Array.isArray(info.delivery)).to.equal(true); 48 | }); 49 | 50 | describe('Replies', function () { 51 | let replyMsg; 52 | 53 | it('can reply to a message', async function () { 54 | replyMsg = await message.reply('this is my reply'); 55 | expect(replyMsg.hasQuotedMsg).to.equal(true); 56 | }); 57 | 58 | it('can get the quoted message', async function () { 59 | const quotedMsg = await replyMsg.getQuotedMessage(); 60 | expect(quotedMsg.id._serialized).to.equal(message.id._serialized); 61 | }); 62 | }); 63 | 64 | describe('Star', function () { 65 | it('can star a message', async function () { 66 | expect(message.isStarred).to.equal(false); 67 | await message.star(); 68 | 69 | await helper.sleep(1000); 70 | 71 | // reload and check 72 | await message.reload(); 73 | expect(message.isStarred).to.equal(true); 74 | }); 75 | 76 | it('can un-star a message', async function () { 77 | expect(message.isStarred).to.equal(true); 78 | await message.unstar(); 79 | 80 | await helper.sleep(1000); 81 | 82 | // reload and check 83 | await message.reload(); 84 | expect(message.isStarred).to.equal(false); 85 | }); 86 | }); 87 | 88 | describe('Delete', function () { 89 | it('can delete a message for me', async function () { 90 | await message.delete(); 91 | 92 | await helper.sleep(5000); 93 | expect(await message.reload()).to.equal(null); 94 | }); 95 | 96 | it('can delete a message for everyone', async function () { 97 | message = await chat.sendMessage('sneaky message'); 98 | await helper.sleep(1000); 99 | 100 | const callback = sinon.spy(); 101 | client.once('message_revoke_everyone', callback); 102 | 103 | await message.delete(true); 104 | await helper.sleep(1000); 105 | 106 | expect(await message.reload()).to.equal(null); 107 | expect(callback.called).to.equal(true); 108 | const [ revokeMsg, originalMsg ] = callback.args[0]; 109 | expect(revokeMsg.id._serialized).to.equal(originalMsg.id._serialized); 110 | expect(originalMsg.body).to.equal('sneaky message'); 111 | expect(originalMsg.type).to.equal('chat'); 112 | expect(revokeMsg.body).to.equal(''); 113 | expect(revokeMsg.type).to.equal('revoked'); 114 | }); 115 | }); 116 | }); -------------------------------------------------------------------------------- /tools/changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | LAST_TAG=$(git describe --tags --abbrev=0) 4 | git log --pretty="%h - %s" "$LAST_TAG"..HEAD -------------------------------------------------------------------------------- /tools/publish: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "$0")" 4 | cd '..' 5 | 6 | BRANCH=`git rev-parse --abbrev-ref HEAD` 7 | RELEASE_MODE=$1 8 | 9 | echo "" 10 | echo "-----> CHECK INPUTS" 11 | echo "" 12 | 13 | if [[ "$RELEASE_MODE" == "alpha" ]] 14 | then 15 | PRERELEASE='true' 16 | VERSION_ARGS="prerelease --preid alpha" 17 | DIST_TAG="next" 18 | elif [[ "$RELEASE_MODE" == "alpha-minor" ]] 19 | then 20 | PRERELEASE='true' 21 | VERSION_ARGS="preminor --preid alpha" 22 | DIST_TAG="next" 23 | elif [[ "$RELEASE_MODE" == "alpha-major" ]] 24 | then 25 | PRERELEASE='true' 26 | VERSION_ARGS="premajor --preid alpha" 27 | DIST_TAG="next" 28 | else 29 | echo 'Release Mode required' 30 | exit 1 31 | fi 32 | 33 | if [ -f ~/.npmrc ]; then 34 | echo "Found existing .npmrc" 35 | else 36 | if [[ -z "$NPM_TOKEN" ]];then 37 | echo "No NPM_TOKEN or ~/.npmrc, exiting.." 38 | exit 1; 39 | else 40 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 41 | fi 42 | fi 43 | echo "Publishing as NPM user: `npm whoami`" 44 | 45 | if [[ $BRANCH != 'main' ]]; then 46 | echo "Not on 'main' branch. Exiting" 47 | exit 1 48 | fi 49 | 50 | if [[ -n $(git status -s) ]]; then 51 | echo "There are uncommitted changes on this branch. Exiting..." 52 | exit 1 53 | fi 54 | 55 | echo "" 56 | echo "-----> BUMP VERSION" 57 | echo "" 58 | 59 | npm version $VERSION_ARGS || exit 1 60 | git push && git push --tags || exit 1 61 | 62 | NEW_VERSION=`cat package.json | jq -r .version` 63 | echo "New Version: $NEW_VERSION" 64 | 65 | echo "" 66 | echo "-----> PUSH TO NPM" 67 | echo "" 68 | 69 | npm publish --tag $DIST_TAG 70 | 71 | 72 | echo "" 73 | echo "-----> Done!" 74 | echo "Version $NEW_VERSION published to $DIST_TAG tag" 75 | echo "" 76 | 77 | echo "::set-output name=NEW_VERSION::$NEW_VERSION" 78 | echo "::set-output name=PRERELEASE::$PRERELEASE" 79 | 80 | exit 0 81 | -------------------------------------------------------------------------------- /tools/version-checker/.version: -------------------------------------------------------------------------------- 1 | 2.2333.11 -------------------------------------------------------------------------------- /tools/version-checker/update-version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const fetch = require('node-fetch'); 5 | 6 | const getLatestVersion = async (currentVersion) => { 7 | const res = await fetch(`https://web.whatsapp.com/check-update?version=${currentVersion}&platform=web`); 8 | const data = await res.json(); 9 | return data.currentVersion; 10 | }; 11 | 12 | const getCurrentVersion = () => { 13 | try { 14 | const versionFile = fs.readFileSync('./.version'); 15 | return versionFile ? versionFile.toString().trim() : null; 16 | } catch(_) { 17 | return null; 18 | } 19 | }; 20 | 21 | const updateInFile = (filePath, oldVersion, newVersion) => { 22 | const originalFile = fs.readFileSync(filePath); 23 | const newFile = originalFile.toString().replaceAll(oldVersion, newVersion); 24 | 25 | fs.writeFileSync(filePath, newFile); 26 | }; 27 | 28 | const updateVersion = async (oldVersion, newVersion) => { 29 | const filesToUpdate = [ 30 | '../../src/util/Constants.js', 31 | '../../README.md', 32 | ]; 33 | 34 | for (const file of filesToUpdate) { 35 | updateInFile(file, oldVersion, newVersion); 36 | } 37 | 38 | fs.writeFileSync('./.version', newVersion); 39 | }; 40 | 41 | (async () => { 42 | const currentVersion = getCurrentVersion(); 43 | const version = await getLatestVersion(currentVersion); 44 | 45 | console.log(`Current version: ${currentVersion}`); 46 | console.log(`Latest version: ${version}`); 47 | 48 | if(currentVersion !== version) { 49 | console.log('Updating files...'); 50 | await updateVersion(currentVersion, version); 51 | console.log('Updated!'); 52 | } else { 53 | console.log('No changes.'); 54 | } 55 | })(); 56 | --------------------------------------------------------------------------------