├── .codebeatignore ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── UPDATES.md ├── img ├── auth.gif ├── logo.jpg ├── logo.png └── logo.svg ├── package-lock.json ├── package.json ├── src ├── api │ ├── helpers │ │ ├── base64-mimetype.ts │ │ ├── decrypt.ts │ │ ├── exposed.enum.ts │ │ ├── file-to-base64.ts │ │ └── index.ts │ ├── layers │ │ ├── README.md │ │ ├── business.layer.ts │ │ ├── controls.layer.ts │ │ ├── group.layer.ts │ │ ├── host.layer.ts │ │ ├── listener.layer.ts │ │ ├── profile.layer.ts │ │ ├── retriever.layer.ts │ │ ├── sender.layer.ts │ │ └── ui.layer.ts │ ├── model │ │ ├── ack.ts │ │ ├── chat.ts │ │ ├── contact-status.ts │ │ ├── contact.ts │ │ ├── enum │ │ │ ├── ack-type.ts │ │ │ ├── chat-state.ts │ │ │ ├── definitions.ts │ │ │ ├── group-change-event.ts │ │ │ ├── group-notification-type.ts │ │ │ ├── index.ts │ │ │ ├── message-type.ts │ │ │ └── socket-state.ts │ │ ├── group-creation.ts │ │ ├── group-metadata.ts │ │ ├── host-device.ts │ │ ├── id.ts │ │ ├── index.ts │ │ ├── live-location.ts │ │ ├── message.ts │ │ ├── partial-message.ts │ │ ├── participant-event.ts │ │ └── whatsapp-profile.ts │ └── whatsapp.ts ├── config │ ├── create-config.ts │ └── puppeteer.config.ts ├── controllers │ ├── auth.ts │ ├── browser.ts │ ├── constants │ │ └── html-auth-query.ts │ └── initializer.ts ├── index.ts ├── lib │ ├── README.md │ ├── jsQR │ │ ├── README.md │ │ ├── gulpfile.js │ │ └── jsQR.js │ ├── middleware.js │ ├── middleware │ │ ├── middleware.ts │ │ ├── tsconfig.json │ │ └── webpack.config.js │ └── wapi │ │ ├── business │ │ ├── get-business-profiles-products.js │ │ ├── send-message-with-buttons.js │ │ └── send-payment-request.js │ │ ├── functions │ │ ├── add-participant.js │ │ ├── are-all-messages-loaded.js │ │ ├── block-contact.js │ │ ├── clear-chat.js │ │ ├── create-group.js │ │ ├── delete-conversation.js │ │ ├── delete-messages.js │ │ ├── demote-participant.js │ │ ├── download-file-with-credentials.js │ │ ├── encrypt-and-upload-file.js │ │ ├── forward-messages.js │ │ ├── get-all-chats-ids.js │ │ ├── get-all-chats-with-messages.js │ │ ├── get-all-chats.js │ │ ├── get-all-contacts.js │ │ ├── get-all-group-metadata.js │ │ ├── get-all-groups.js │ │ ├── get-all-messages-in-chat.js │ │ ├── get-all-new-messages.js │ │ ├── get-all-unread-messages.js │ │ ├── get-battery-level.js │ │ ├── get-chat-by-id.js │ │ ├── get-chat-by-name.js │ │ ├── get-chat.js │ │ ├── get-chats-with-new-messages.js │ │ ├── get-common-groups.js │ │ ├── get-contact.js │ │ ├── get-group-admins.js │ │ ├── get-group-invite-link.js │ │ ├── get-group-metadata.js │ │ ├── get-group-participant-ids.js │ │ ├── get-group-participants.js │ │ ├── get-host.js │ │ ├── get-me.js │ │ ├── get-message-by-id.js │ │ ├── get-my-contacts.js │ │ ├── get-new-id.js │ │ ├── get-new-message-id.js │ │ ├── get-number-profile.js │ │ ├── get-profile-pic-from-server.js │ │ ├── get-status.js │ │ ├── get-unread-messages-in-chat.js │ │ ├── get-unread-messages.js │ │ ├── has-unread-messages.js │ │ ├── index.js │ │ ├── is-connected.js │ │ ├── is-logged-in.js │ │ ├── leave-group.js │ │ ├── load-all-earlier-chat-messages.js │ │ ├── load-and-get-all-messages-in-chat.js │ │ ├── load-earlier-chat-messages.js │ │ ├── load-earlier-messages-til-date.js │ │ ├── process-files.js │ │ ├── process-message-object.js │ │ ├── promote-participant.js │ │ ├── remove-participant.js │ │ ├── reply.js │ │ ├── revoke-invite-link.js │ │ ├── send-chat-state.js │ │ ├── send-contact.js │ │ ├── send-file.js │ │ ├── send-image-as-stricker.js │ │ ├── send-image-with-product.js │ │ ├── send-image.js │ │ ├── send-location.js │ │ ├── send-message-to-id.js │ │ ├── send-message-with-tags.js │ │ ├── send-message-with-thumb.js │ │ ├── send-message.js │ │ ├── send-message2.js │ │ ├── send-seen.js │ │ ├── send-sticker.js │ │ ├── send-video-as-gif.js │ │ ├── set-my-name.js │ │ ├── set-my-status.js │ │ ├── simulate-typing.js │ │ └── unblock-contact.js │ │ ├── helper │ │ ├── array-buffer-to-base64.js │ │ ├── base64-to-file.js │ │ ├── generate-media-key.js │ │ ├── get-file-hash.js │ │ ├── index.js │ │ └── is-chat-message.js │ │ ├── jsconfig.json │ │ ├── jssha │ │ └── index.js │ │ ├── listeners │ │ ├── add-all-new-messages.js │ │ ├── add-new-messages.js │ │ ├── add-on-added-to-group.js │ │ ├── add-on-live-location.js │ │ ├── add-on-new-ack.js │ │ ├── add-on-participants-change.js │ │ ├── add-on-state-change.js │ │ ├── index.js │ │ ├── init-common-listener.js │ │ └── init-listeners.js │ │ ├── serializers │ │ ├── index.js │ │ ├── serialize-chat.js │ │ ├── serialize-contact.js │ │ ├── serialize-message.js │ │ ├── serialize-number-status.js │ │ ├── serialize-profile-pic-thumb.js │ │ └── serialize-raw.js │ │ ├── store │ │ ├── get-store.js │ │ └── store-objects.js │ │ ├── wapi.js │ │ └── webpack.config.js └── utils │ ├── semver.ts │ ├── sleep.ts │ └── wait-for-response.ts └── tsconfig.json /.codebeatignore: -------------------------------------------------------------------------------- 1 | src/lib/jsQR/jsQR.js 2 | src/lib/wapi/jssha/index.js 3 | docs/ 4 | docs/assets/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | session 4 | session-* 5 | app.js 6 | app.ts 7 | app2.js 8 | gohan.jpg 9 | 006.png 10 | src/app.ts 11 | assets -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src 3 | session 4 | assets 5 | session-* 6 | assets 7 | img 8 | docs 9 | CONTRIBUTING.md 10 | UPDATES.md 11 | app.ts 12 | app.js 13 | app2.js 14 | LICENSE 15 | CHANGELOG.md 16 | .codebeatignore 17 | .prettierignore 18 | .prettierrc -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | docs -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | PRs are welcome. Just please mantain the code as clean as possible and use common JS conventions 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Daniel Cardenas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /UPDATES.md: -------------------------------------------------------------------------------- 1 | # Update checking 2 | 3 | Whatsapp is in constant change, in order to tackle this issue I suggest 4 | keeping your sulla package always up-to-date. 5 | 6 | The method/funciton names won't change, only their core algorithm. This way you won't have to makes changes in your code at every update. They will remain the same forever. 7 | -------------------------------------------------------------------------------- /img/auth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielcardeenas/sulla/64e2621a00ef25325ca09796831b3bd699693043/img/auth.gif -------------------------------------------------------------------------------- /img/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielcardeenas/sulla/64e2621a00ef25325ca09796831b3bd699693043/img/logo.jpg -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielcardeenas/sulla/64e2621a00ef25325ca09796831b3bd699693043/img/logo.png -------------------------------------------------------------------------------- /img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 15 | 17 | 18 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sulla", 3 | "version": "2.4.0", 4 | "description": "Javascript whatsapp framework", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "start": "npm run build:sulla & tsc app.ts && node --trace-warnings app.js", 9 | "build": "npm run build:wapi && npm run build:middleware && npm run build:jsQR && tsc", 10 | "build:sulla": "tsc", 11 | "build:wapi": "cd src/lib/wapi/ && webpack", 12 | "build:middleware": "cd src/lib/middleware/ && webpack", 13 | "build:jsQR": "cd src/lib/jsQR/ && gulp", 14 | "build:docs": "typedoc && git add docs/*", 15 | "watch": "concurrently \"tsc -w\" \"nodemon dist/index.js\"", 16 | "clean": "rm -rf session && rm -rf dist", 17 | "test": "echo \"No tests yet\"", 18 | "changelog": "auto-changelog -p && git add CHANGELOG.md", 19 | "release": "read -p 'GITHUB_TOKEN: ' GITHUB_TOKEN && export GITHUB_TOKEN=$GITHUB_TOKEN && release-it" 20 | }, 21 | "release-it": { 22 | "github": { 23 | "release": true 24 | } 25 | }, 26 | "auto-changelog": { 27 | "commitLimit": false 28 | }, 29 | "husky": { 30 | "hooks": { 31 | "pre-commit": "pretty-quick --staged" 32 | } 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/danielcardeenas/sulla.git" 37 | }, 38 | "keywords": [ 39 | "whatsapp", 40 | "javascript", 41 | "bot", 42 | "typescript", 43 | "automatization", 44 | "puppeteer" 45 | ], 46 | "author": "Daniel Cardenas", 47 | "license": "ISC", 48 | "bugs": { 49 | "url": "https://github.com/danielcardeenas/sulla/issues" 50 | }, 51 | "homepage": "https://github.com/danielcardeenas/sulla#readme", 52 | "devDependencies": { 53 | "@types/node": "^15.12.4", 54 | "@types/puppeteer": "^5.4.3", 55 | "@types/sharp": "^0.27.1", 56 | "auto-changelog": "^2.2.1", 57 | "concurrently": "^5.3.0", 58 | "copy-webpack-plugin": "^7.0.0", 59 | "gulp": "^4.0.2", 60 | "husky": "^5.0.9", 61 | "nodemon": "^2.0.7", 62 | "prettier": "^2.2.1", 63 | "pretty-quick": "^3.1.0", 64 | "release-it": "^14.4.0", 65 | "ts-loader": "^8.0.17", 66 | "typedoc": "^0.20.25", 67 | "typescript": "^4.1.5", 68 | "webpack": "^5.22.0", 69 | "webpack-cli": "^4.5.0" 70 | }, 71 | "dependencies": { 72 | "boxen": "^5.0.0", 73 | "chalk": "^4.1.0", 74 | "chrome-launcher": "^0.13.4", 75 | "file-type": "^16.2.0", 76 | "futoin-hkdf": "^1.3.3", 77 | "latest-version": "^5.1.0", 78 | "puppeteer": "^5.3.1", 79 | "puppeteer-extra": "^3.1.17", 80 | "puppeteer-extra-plugin-stealth": "^2.7.5", 81 | "qrcode-terminal": "^0.12.0", 82 | "rxjs": "^6.6.3", 83 | "sharp": "^0.27.1", 84 | "spinnies": "^0.5.1" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/api/helpers/base64-mimetype.ts: -------------------------------------------------------------------------------- 1 | export function base64MimeType(encoded: string) { 2 | let result = null; 3 | if (typeof encoded !== 'string') { 4 | return result; 5 | } 6 | 7 | const mime = encoded.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+).*,.*/); 8 | if (mime && mime.length) { 9 | result = mime[1]; 10 | } 11 | 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /src/api/helpers/decrypt.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | import * as hkdf from 'futoin-hkdf'; 3 | import { Message } from '../model'; 4 | import { MediaType } from '../model/enum'; 5 | 6 | /** 7 | * Decrypts given message file 8 | * @param encBase64 .enc file as base64 9 | * @param message Message object 10 | * @returns dectypted file as buffer 11 | */ 12 | export function decrypt(encBase64: string, message: Message) { 13 | const encBuffer = Buffer.from(encBase64, 'base64'); 14 | const file = generateFile(encBuffer, message.mediaKey, message.type); 15 | return file; 16 | } 17 | 18 | /** 19 | * Generates buffer file from enc file and media key 20 | * @param encBuffer 21 | * @param mediaKeyBase64 22 | * @param mediaType 23 | */ 24 | function generateFile( 25 | encBuffer: Buffer, 26 | mediaKeyBase64: string, 27 | mediaType: string 28 | ) { 29 | // Generic derivation 30 | const expandedDerivation = expandDerivation(mediaType, mediaKeyBase64); 31 | 32 | // Dechipher 33 | const deciphed = decipher(expandedDerivation); 34 | 35 | // Decode 36 | encBuffer = encBuffer.slice(0, -10); 37 | const decoded = deciphed.update(encBuffer); 38 | const fileBuffer = Buffer.from(decoded as any, 'utf-8'); 39 | return fileBuffer; 40 | } 41 | 42 | /** 43 | * Executes HMAC-based Extract-and-Expand Key Derivation Function (HKDF). 44 | * @param mediaType 45 | * @param mediaKeyBase64 46 | */ 47 | function expandDerivation(mediaType: string, mediaKeyBase64: string) { 48 | const options: hkdf.Options = { 49 | salt: new Uint8Array(32) as any, 50 | info: `WhatsApp ${MediaType[mediaType.toUpperCase()]} Keys`, 51 | hash: 'sha256', 52 | }; 53 | 54 | // required output length in bytes 55 | const length = 112; 56 | const mediaKeyBuffer = Buffer.from(mediaKeyBase64, 'base64'); 57 | 58 | // Generic derivation 59 | return hkdf(mediaKeyBuffer, length, options); 60 | } 61 | 62 | function decipher(expandedDerivation: Buffer) { 63 | const cropped = expandedDerivation.slice(0, 16); 64 | const cipherKey = expandedDerivation.slice(16, 48); 65 | return crypto.createDecipheriv('aes-256-cbc', cipherKey, cropped); 66 | } 67 | -------------------------------------------------------------------------------- /src/api/helpers/exposed.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ExposedFn { 2 | OnMessage = 'onMessage', 3 | OnAnyMessage = 'onAnyMessage', 4 | onAck = 'onAck', 5 | onParticipantsChanged = 'onParticipantsChanged', 6 | onStateChange = 'onStateChange', 7 | } 8 | -------------------------------------------------------------------------------- /src/api/helpers/file-to-base64.ts: -------------------------------------------------------------------------------- 1 | import * as FileType from 'file-type'; 2 | import * as fs from 'fs'; 3 | 4 | /** 5 | * Converts given file into base64 string 6 | * @param path file path 7 | * @param mime Optional, will retrieve file mime automatically if not defined (Example: 'image/png') 8 | */ 9 | export async function fileToBase64(path: string, mime?: string) { 10 | const base64 = fs.readFileSync(path, { encoding: 'base64' }); 11 | if (mime === undefined) { 12 | const fileType = await FileType.fromFile(path); 13 | mime = fileType.mime; 14 | } 15 | const data = `data:${mime};base64,${base64}`; 16 | return data; 17 | } 18 | -------------------------------------------------------------------------------- /src/api/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export { fileToBase64 } from './file-to-base64'; 2 | export { base64MimeType } from './base64-mimetype'; 3 | -------------------------------------------------------------------------------- /src/api/layers/README.md: -------------------------------------------------------------------------------- 1 | # Layers 2 | 3 | #### Each layer should extends the previous one in the next order: 4 | 5 | 1. Host layer 6 | 2. Profile layer 7 | 3. Listener layer 8 | 4. Sender layer 9 | 5. Retriever layer 10 | 6. Group layer 11 | 7. Controls layer 12 | 8. Business layer (Optional) 13 | 14 | **Controls layer** should be enough 15 | -------------------------------------------------------------------------------- /src/api/layers/business.layer.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { ControlsLayer } from './controls.layer'; 3 | 4 | declare module WAPI { 5 | const getBusinessProfilesProducts: (to: string) => any; 6 | const sendImageWithProduct: ( 7 | base64: string, 8 | to: string, 9 | caption: string, 10 | bizNumber: string, 11 | productId: string 12 | ) => any; 13 | } 14 | 15 | export class BusinessLayer extends ControlsLayer { 16 | constructor(page: Page) { 17 | super(page); 18 | } 19 | 20 | /** 21 | * Querys product catalog 22 | * @param id Buisness profile id ('00000@c.us') 23 | */ 24 | public async getBusinessProfilesProducts(id: string) { 25 | return this.page.evaluate( 26 | ({ id }) => { 27 | WAPI.getBusinessProfilesProducts(id); 28 | }, 29 | { id } 30 | ); 31 | } 32 | 33 | /** 34 | * Sends product with product image to given chat id 35 | * @param to Chat id 36 | * @param base64 Base64 image data 37 | * @param caption Message body 38 | * @param businessId Business id number that owns the product ('0000@c.us') 39 | * @param productId Product id, see method getBusinessProfilesProducts for more info 40 | */ 41 | public async sendImageWithProduct( 42 | to: string, 43 | base64: string, 44 | caption: string, 45 | businessId: string, 46 | productId: string 47 | ) { 48 | return this.page.evaluate( 49 | ({ to, base64, businessId, caption, productId }) => { 50 | WAPI.sendImageWithProduct(base64, to, caption, businessId, productId); 51 | }, 52 | { to, base64, businessId, caption, productId } 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/api/layers/controls.layer.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { GroupLayer } from './group.layer'; 3 | import { UILayer } from './ui.layer'; 4 | 5 | declare module WAPI { 6 | const deleteConversation: (chatId: string) => boolean; 7 | const clearChat: (chatId: string) => void; 8 | const deleteMessages: ( 9 | contactId: string, 10 | messageId: string[] | string, 11 | onlyLocal: boolean 12 | ) => any; 13 | } 14 | 15 | export class ControlsLayer extends UILayer { 16 | constructor(page: Page) { 17 | super(page); 18 | } 19 | 20 | /** 21 | * Deletes the given chat 22 | * @param chatId 23 | * @returns boolean 24 | */ 25 | public async deleteChat(chatId: string) { 26 | return this.page.evaluate( 27 | (chatId) => WAPI.deleteConversation(chatId), 28 | chatId 29 | ); 30 | } 31 | 32 | /** 33 | * Deletes all messages of given chat 34 | * @param chatId 35 | * @returns boolean 36 | */ 37 | public async clearChat(chatId: string) { 38 | return this.page.evaluate((chatId) => WAPI.clearChat(chatId), chatId); 39 | } 40 | 41 | /** 42 | * Deletes message of given message id 43 | * @param chatId The chat id from which to delete the message. 44 | * @param messageId The specific message id of the message to be deleted 45 | * @param onlyLocal If it should only delete locally (message remains on the other recipienct's phone). Defaults to false. 46 | */ 47 | public async deleteMessage( 48 | chatId: string, 49 | messageId: string[] | string, 50 | onlyLocal = false 51 | ) { 52 | return await this.page.evaluate( 53 | ({ contactId, messageId, onlyLocal }) => 54 | WAPI.deleteMessages(contactId, messageId, onlyLocal), 55 | { contactId: chatId, messageId, onlyLocal } 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/api/layers/group.layer.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { Contact, GroupCreation, Id } from '../model'; 3 | import { RetrieverLayer } from './retriever.layer'; 4 | 5 | declare module WAPI { 6 | const leaveGroup: (groupId: string) => any; 7 | const getGroupParticipantIDs: (groupId: string) => Id[]; 8 | const getGroupInviteLink: (chatId: string) => Promise; 9 | const createGroup: ( 10 | groupName: string, 11 | contactId: string | string[] 12 | ) => GroupCreation; 13 | const removeParticipant: (groupId: string, contactId: string) => void; 14 | const addParticipant: (groupId: string, contactId: string) => void; 15 | const promoteParticipant: (groupId: string, contactId: string) => void; 16 | const demoteParticipant: (groupId: string, contactId: string) => void; 17 | const getGroupAdmins: (groupId: string) => Contact[]; 18 | } 19 | 20 | export class GroupLayer extends RetrieverLayer { 21 | constructor(page: Page) { 22 | super(page); 23 | } 24 | 25 | /** 26 | * Removes the host device from the group 27 | * @param groupId group id 28 | */ 29 | public async leaveGroup(groupId: string) { 30 | return this.page.evaluate((groupId) => WAPI.leaveGroup(groupId), groupId); 31 | } 32 | 33 | /** 34 | * Retrieves group members as [Id] objects 35 | * @param groupId group id 36 | */ 37 | public async getGroupMembersIds(groupId: string): Promise { 38 | return this.page.evaluate( 39 | (groupId: string) => WAPI.getGroupParticipantIDs(groupId), 40 | groupId 41 | ); 42 | } 43 | 44 | /** 45 | * Returns group members [Contact] objects 46 | * @param groupId 47 | */ 48 | public async getGroupMembers(groupId: string) { 49 | const membersIds = await this.getGroupMembersIds(groupId); 50 | const actions = membersIds.map((memberId) => { 51 | return this.getContact(memberId._serialized); 52 | }); 53 | return Promise.all(actions); 54 | } 55 | 56 | /** 57 | * Generates group-invite link 58 | * @param chatId 59 | * @returns Invitation link 60 | */ 61 | public async getGroupInviteLink(chatId: string) { 62 | return await this.page.evaluate( 63 | (chatId) => WAPI.getGroupInviteLink(chatId), 64 | chatId 65 | ); 66 | } 67 | 68 | /** 69 | * Creates a new chat group 70 | * @param groupName Group name 71 | * @param contacts Contacts that should be added. 72 | */ 73 | public async createGroup(groupName: string, contacts: string | string[]) { 74 | return await this.page.evaluate( 75 | ({ groupName, contacts }) => WAPI.createGroup(groupName, contacts), 76 | { groupName, contacts } 77 | ); 78 | } 79 | 80 | /** 81 | * Removes participant from group 82 | * @param groupId Chat id ('0000000000-00000000@g.us') 83 | * @param participantId Participant id'000000000000@c.us' 84 | */ 85 | public async removeParticipant(groupId: string, participantId: string) { 86 | return await this.page.evaluate( 87 | ({ groupId, participantId }) => 88 | WAPI.removeParticipant(groupId, participantId), 89 | { groupId, participantId } 90 | ); 91 | } 92 | 93 | /** 94 | * Adds participant to Group 95 | * @param groupId Chat id ('0000000000-00000000@g.us') 96 | * @param participantId Participant id'000000000000@c.us' 97 | */ 98 | public async addParticipant(groupId: string, participantId: string) { 99 | return await this.page.evaluate( 100 | ({ groupId, participantId }) => 101 | WAPI.addParticipant(groupId, participantId), 102 | { groupId, participantId } 103 | ); 104 | } 105 | 106 | /** 107 | * Promotes participant as Admin in given group 108 | * @param groupId Chat id ('0000000000-00000000@g.us') 109 | * @param participantId Participant id'000000000000@c.us' 110 | */ 111 | public async promoteParticipant(groupId: string, participantId: string) { 112 | return await this.page.evaluate( 113 | ({ groupId, participantId }) => 114 | WAPI.promoteParticipant(groupId, participantId), 115 | { groupId, participantId } 116 | ); 117 | } 118 | 119 | /** 120 | * Demotes admin privileges of participant 121 | * @param groupId Chat id ('0000000000-00000000@g.us') 122 | * @param participantId Participant id'000000000000@c.us' 123 | */ 124 | public async demoteParticipant(groupId: string, participantId: string) { 125 | return await this.page.evaluate( 126 | ({ groupId, participantId }) => 127 | WAPI.demoteParticipant(groupId, participantId), 128 | { groupId, participantId } 129 | ); 130 | } 131 | 132 | /** 133 | * Retrieves group admins 134 | * @param chatId Group/Chat id ('0000000000-00000000@g.us') 135 | */ 136 | public async getGroupAdmins(chatId: string) { 137 | return await this.page.evaluate( 138 | (chatId) => WAPI.getGroupAdmins(chatId), 139 | chatId 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/api/layers/host.layer.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { HostDevice } from '../model'; 3 | import { SocketState } from '../model/enum'; 4 | 5 | declare module WAPI { 6 | const getHost: () => HostDevice; 7 | const getWAVersion: () => string; 8 | const isConnected: () => boolean; 9 | const getBatteryLevel: () => number; 10 | } 11 | 12 | export class HostLayer { 13 | constructor(public page: Page) { 14 | this.page = page; 15 | } 16 | /** 17 | * @returns Current host device details 18 | */ 19 | public async getHostDevice() { 20 | return await this.page.evaluate(() => WAPI.getHost()); 21 | } 22 | 23 | /** 24 | * Retrieves WA version 25 | */ 26 | public async getWAVersion() { 27 | return await this.page.evaluate(() => WAPI.getWAVersion()); 28 | } 29 | 30 | /** 31 | * Retrieves the connecction state 32 | */ 33 | public async getConnectionState(): Promise { 34 | return await this.page.evaluate(() => { 35 | //@ts-ignore 36 | return Store.State.default.state; 37 | }); 38 | } 39 | 40 | /** 41 | * Retrieves if the phone is online. Please note that this may not be real time. 42 | */ 43 | public async isConnected() { 44 | return await this.page.evaluate(() => WAPI.isConnected()); 45 | } 46 | 47 | /** 48 | * Retrieves Battery Level 49 | */ 50 | public async getBatteryLevel() { 51 | return await this.page.evaluate(() => WAPI.getBatteryLevel()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/api/layers/listener.layer.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { ExposedFn } from '../helpers/exposed.enum'; 3 | import { Ack, Chat, LiveLocation, Message, ParticipantEvent } from '../model'; 4 | import { SocketState } from '../model/enum'; 5 | import { ProfileLayer } from './profile.layer'; 6 | 7 | declare module WAPI { 8 | const waitNewMessages: (rmCallback: boolean, callback: Function) => void; 9 | const allNewMessagesListener: (callback: Function) => void; 10 | const onStateChange: (callback: Function) => void; 11 | const onAddedToGroup: (callback: Function) => any; 12 | const onParticipantsChanged: (groupId: string, callback: Function) => any; 13 | const onLiveLocation: (chatId: string, callback: Function) => any; 14 | } 15 | 16 | export class ListenerLayer extends ProfileLayer { 17 | constructor(public page: Page) { 18 | super(page); 19 | } 20 | 21 | /** 22 | * Listens to messages received 23 | * @returns Observable stream of messages 24 | */ 25 | public onMessage(fn: (message: Message) => void) { 26 | this.page.exposeFunction(ExposedFn.OnMessage, (message: Message) => 27 | fn(message) 28 | ); 29 | } 30 | 31 | /** 32 | * @event Listens to all new messages 33 | * @param to callback 34 | * @fires Message 35 | */ 36 | public async onAnyMessage(fn: (message: Message) => void) { 37 | this.page 38 | .exposeFunction(ExposedFn.OnAnyMessage, (message: Message) => fn(message)) 39 | .then((_) => 40 | this.page.evaluate(() => { 41 | WAPI.allNewMessagesListener(window['onAnyMessage']); 42 | }) 43 | ); 44 | } 45 | 46 | /** 47 | * @event Listens to messages received 48 | * @returns Observable stream of messages 49 | */ 50 | public onStateChange(fn: (state: SocketState) => void) { 51 | this.page 52 | .exposeFunction(ExposedFn.onStateChange, (state: SocketState) => 53 | fn(state) 54 | ) 55 | .then(() => 56 | this.page.evaluate(() => { 57 | WAPI.onStateChange((_) => window['onStateChange'](_.state)); 58 | }) 59 | ); 60 | } 61 | 62 | /** 63 | * @event Listens to messages acknowledgement Changes 64 | * @returns Observable stream of messages 65 | */ 66 | public onAck(fn: (ack: Ack) => void) { 67 | this.page.exposeFunction(ExposedFn.onAck, (ack: Ack) => fn(ack)); 68 | } 69 | 70 | /** 71 | * @event Listens to live locations from a chat that already has valid live locations 72 | * @param chatId the chat from which you want to subscribes to live location updates 73 | * @param fn callback that takes in a LiveLocation 74 | * @returns boolean, if returns false then there were no valid live locations in the chat of chatId 75 | * @emits LiveLocation 76 | */ 77 | public async onLiveLocation( 78 | chatId: string, 79 | fn: (liveLocationChangedEvent: LiveLocation) => void 80 | ) { 81 | const method = 'onLiveLocation_' + chatId.replace('_', '').replace('_', ''); 82 | return this.page 83 | .exposeFunction(method, (liveLocationChangedEvent: LiveLocation) => 84 | fn(liveLocationChangedEvent) 85 | ) 86 | .then((_) => 87 | this.page.evaluate( 88 | ({ chatId, method }) => { 89 | //@ts-ignore 90 | return WAPI.onLiveLocation(chatId, window[method]); 91 | }, 92 | { chatId, method } 93 | ) 94 | ); 95 | } 96 | 97 | /** 98 | * @param to group id: xxxxx-yyyy@us.c 99 | * @param to callback 100 | * @returns Stream of ParticipantEvent 101 | */ 102 | public async onParticipantsChanged( 103 | groupId: string, 104 | fn: (participantChangedEvent: ParticipantEvent) => void 105 | ) { 106 | const method = 107 | 'onParticipantsChanged_' + groupId.replace('_', '').replace('_', ''); 108 | return this.page 109 | .exposeFunction(method, (participantChangedEvent: ParticipantEvent) => 110 | fn(participantChangedEvent) 111 | ) 112 | .then((_) => 113 | this.page.evaluate( 114 | ({ groupId, method }) => { 115 | //@ts-ignore 116 | WAPI.onParticipantsChanged(groupId, window[method]); 117 | }, 118 | { groupId, method } 119 | ) 120 | ); 121 | } 122 | 123 | /** 124 | * @event Fires callback with Chat object every time the host phone is added to a group. 125 | * @param to callback 126 | * @returns Observable stream of Chats 127 | */ 128 | public async onAddedToGroup(fn: (chat: Chat) => any) { 129 | const method = 'onAddedToGroup'; 130 | return this.page 131 | .exposeFunction(method, (chat: any) => fn(chat)) 132 | .then((_) => 133 | this.page.evaluate(() => { 134 | //@ts-ignore 135 | WAPI.onAddedToGroup(window.onAddedToGroup); 136 | }) 137 | ); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/api/layers/profile.layer.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { HostLayer } from './host.layer'; 3 | 4 | declare module WAPI { 5 | const setMyStatus: (to: string) => void; 6 | const setMyName: (name: string) => void; 7 | } 8 | 9 | export class ProfileLayer extends HostLayer { 10 | constructor(public page: Page) { 11 | super(page); 12 | } 13 | 14 | /** 15 | * Sets current user profile status 16 | * @param status 17 | */ 18 | public async setProfileStatus(status: string) { 19 | return await this.page.evaluate( 20 | ({ status }) => { 21 | WAPI.setMyStatus(status); 22 | }, 23 | { status } 24 | ); 25 | } 26 | 27 | /** 28 | * Sets current user profile name 29 | * @param name 30 | */ 31 | public async setProfileName(name: string) { 32 | return this.page.evaluate( 33 | ({ name }) => { 34 | WAPI.setMyName(name); 35 | }, 36 | { name } 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/api/layers/retriever.layer.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { 3 | Chat, 4 | Contact, 5 | ContactStatus, 6 | Message, 7 | PartialMessage, 8 | WhatsappProfile, 9 | } from '../model'; 10 | import { SenderLayer } from './sender.layer'; 11 | 12 | declare module WAPI { 13 | const getAllChatsWithNewMsg: () => Chat[]; 14 | const getAllNewMessages: () => any; 15 | const getAllChats: () => Chat[]; 16 | const getAllChatsWithMessages: (withNewMessageOnly?: boolean) => Chat[]; 17 | const getContact: (contactId: string) => Contact; 18 | const getAllContacts: () => Contact[]; 19 | const getChatById: (contactId: string) => Chat; 20 | const getChat: (contactId: string) => Chat; 21 | const getProfilePicFromServer: (chatId: string) => string; 22 | const loadEarlierMessages: (contactId: string) => Message[]; 23 | const getStatus: (contactId: string) => ContactStatus; 24 | const getNumberProfile: (contactId: string) => WhatsappProfile; 25 | const getUnreadMessages: ( 26 | includeMe: boolean, 27 | includeNotifications: boolean, 28 | useUnreadCount: boolean 29 | ) => any; 30 | const getAllUnreadMessages: () => PartialMessage[]; 31 | const getAllMessagesInChat: ( 32 | chatId: string, 33 | includeMe: boolean, 34 | includeNotifications: boolean 35 | ) => Message[]; 36 | const loadAndGetAllMessagesInChat: ( 37 | chatId: string, 38 | includeMe: boolean, 39 | includeNotifications: boolean 40 | ) => Message[]; 41 | } 42 | 43 | export class RetrieverLayer extends SenderLayer { 44 | constructor(page: Page) { 45 | super(page); 46 | } 47 | 48 | /** 49 | * Retrieves all chats 50 | * @returns array of [Chat] 51 | */ 52 | public async getAllChats(withNewMessageOnly = false) { 53 | if (withNewMessageOnly) { 54 | return this.page.evaluate(() => WAPI.getAllChatsWithNewMsg()); 55 | } else { 56 | return this.page.evaluate(() => WAPI.getAllChats()); 57 | } 58 | } 59 | 60 | /** 61 | * Retrieves all chats with messages 62 | * @returns array of [Chat] 63 | */ 64 | public async getAllChatsWithMessages(withNewMessageOnly = false) { 65 | return this.page.evaluate( 66 | (withNewMessageOnly: boolean) => 67 | WAPI.getAllChatsWithMessages(withNewMessageOnly), 68 | withNewMessageOnly 69 | ); 70 | } 71 | 72 | /** 73 | * Retrieve all groups 74 | * @returns array of groups 75 | */ 76 | public async getAllGroups(withNewMessagesOnly = false) { 77 | if (withNewMessagesOnly) { 78 | // prettier-ignore 79 | const chats = await this.page.evaluate(() => WAPI.getAllChatsWithNewMsg()); 80 | return chats.filter((chat) => chat.isGroup); 81 | } else { 82 | const chats = await this.page.evaluate(() => WAPI.getAllChats()); 83 | return chats.filter((chat) => chat.isGroup); 84 | } 85 | } 86 | 87 | /** 88 | * Retrieves contact detail object of given contact id 89 | * @param contactId 90 | * @returns contact detial as promise 91 | */ 92 | public async getContact(contactId: string) { 93 | return this.page.evaluate( 94 | (contactId) => WAPI.getContact(contactId), 95 | contactId 96 | ); 97 | } 98 | 99 | /** 100 | * Retrieves all contacts 101 | * @returns array of [Contact] 102 | */ 103 | public async getAllContacts() { 104 | return await this.page.evaluate(() => WAPI.getAllContacts()); 105 | } 106 | 107 | /** 108 | * Retrieves chat object of given contact id 109 | * @param contactId 110 | * @returns contact detial as promise 111 | */ 112 | public async getChatById(contactId: string) { 113 | return this.page.evaluate( 114 | (contactId) => WAPI.getChatById(contactId), 115 | contactId 116 | ); 117 | } 118 | 119 | /** 120 | * Retrieves chat object of given contact id 121 | * @param contactId 122 | * @returns contact detial as promise 123 | * @deprecated 124 | */ 125 | public async getChat(contactId: string) { 126 | return this.getChatById(contactId); 127 | } 128 | 129 | /** 130 | * Retrieves chat picture 131 | * @param chatId Chat id 132 | * @returns url of the chat picture or undefined if there is no picture for the chat. 133 | */ 134 | public async getProfilePicFromServer(chatId: string) { 135 | return this.page.evaluate( 136 | (chatId) => WAPI.getProfilePicFromServer(chatId), 137 | chatId 138 | ); 139 | } 140 | 141 | /** 142 | * Load more messages in chat object from server. Use this in a while loop 143 | * @param contactId 144 | * @returns contact detial as promise 145 | * @deprecated 146 | */ 147 | public async loadEarlierMessages(contactId: string) { 148 | return this.page.evaluate( 149 | (contactId) => WAPI.loadEarlierMessages(contactId), 150 | contactId 151 | ); 152 | } 153 | 154 | /** 155 | * Retrieves status of given contact 156 | * @param contactId 157 | */ 158 | public async getStatus(contactId: string) { 159 | return this.page.evaluate( 160 | (contactId) => WAPI.getStatus(contactId), 161 | contactId 162 | ); 163 | } 164 | 165 | /** 166 | * Checks if a number is a valid whatsapp number 167 | * @param contactId, you need to include the @c.us at the end. 168 | * @returns contact detial as promise 169 | */ 170 | public async getNumberProfile(contactId: string) { 171 | return this.page.evaluate( 172 | (contactId) => WAPI.getNumberProfile(contactId), 173 | contactId 174 | ); 175 | } 176 | 177 | /** 178 | * Retrieves all undread Messages 179 | * @param includeMe 180 | * @param includeNotifications 181 | * @param useUnreadCount 182 | * @returns any 183 | * @deprecated 184 | */ 185 | public async getUnreadMessages( 186 | includeMe: boolean, 187 | includeNotifications: boolean, 188 | useUnreadCount: boolean 189 | ) { 190 | return await this.page.evaluate( 191 | ({ includeMe, includeNotifications, useUnreadCount }) => 192 | WAPI.getUnreadMessages(includeMe, includeNotifications, useUnreadCount), 193 | { includeMe, includeNotifications, useUnreadCount } 194 | ); 195 | } 196 | 197 | /** 198 | * Retrieves all unread messages (where ack is -1) 199 | * @returns list of messages 200 | */ 201 | public async getAllUnreadMessages() { 202 | return this.page.evaluate(() => WAPI.getAllUnreadMessages()); 203 | } 204 | 205 | /** 206 | * Retrieves all new messages (where isNewMsg is true) 207 | * @returns List of messages 208 | * @deprecated Use getAllUnreadMessages 209 | */ 210 | public async getAllNewMessages() { 211 | return this.page.evaluate(() => WAPI.getAllNewMessages()); 212 | } 213 | 214 | /** 215 | * Retrieves all messages already loaded in a chat 216 | * For loading every message use loadAndGetAllMessagesInChat 217 | * @param chatId, the chat to get the messages from 218 | * @param includeMe, include my own messages? boolean 219 | * @param includeNotifications 220 | * @returns any 221 | */ 222 | public async getAllMessagesInChat( 223 | chatId: string, 224 | includeMe: boolean, 225 | includeNotifications: boolean 226 | ) { 227 | return await this.page.evaluate( 228 | ({ chatId, includeMe, includeNotifications }) => 229 | WAPI.getAllMessagesInChat(chatId, includeMe, includeNotifications), 230 | { chatId, includeMe, includeNotifications } 231 | ); 232 | } 233 | 234 | /** 235 | * Loads and Retrieves all Messages in a chat 236 | * @param chatId, the chat to get the messages from 237 | * @param includeMe, include my own messages? boolean 238 | * @param includeNotifications 239 | * @returns any 240 | */ 241 | public async loadAndGetAllMessagesInChat( 242 | chatId: string, 243 | includeMe = false, 244 | includeNotifications = false 245 | ) { 246 | return await this.page.evaluate( 247 | ({ chatId, includeMe, includeNotifications }) => 248 | WAPI.loadAndGetAllMessagesInChat( 249 | chatId, 250 | includeMe, 251 | includeNotifications 252 | ), 253 | { chatId, includeMe, includeNotifications } 254 | ); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/api/layers/sender.layer.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import * as sharp from 'sharp'; 3 | import { base64MimeType, fileToBase64 } from '../helpers'; 4 | import { Chat, Message } from '../model'; 5 | import { ChatState } from '../model/enum'; 6 | import { ListenerLayer } from './listener.layer'; 7 | 8 | declare module WAPI { 9 | const sendSeen: (to: string) => void; 10 | const getChat: (contactId: string) => Chat; 11 | const startTyping: (to: string) => void; 12 | const stopTyping: (to: string) => void; 13 | const sendMessage: (to: string, content: string) => string; 14 | const sendImage: ( 15 | imgBase64: string, 16 | to: string, 17 | filename: string, 18 | caption: string 19 | ) => any; 20 | const sendMessageWithThumb: ( 21 | thumb: string, 22 | url: string, 23 | title: string, 24 | description: string, 25 | chatId: string 26 | ) => void; 27 | const reply: ( 28 | to: string, 29 | content: string, 30 | quotedMsg: string | Message 31 | ) => void; 32 | const sendFile: ( 33 | base64: string, 34 | to: string, 35 | filename: string, 36 | caption: string 37 | ) => any; 38 | const sendVideoAsGif: ( 39 | base64: string, 40 | to: string, 41 | filename: string, 42 | caption: string 43 | ) => void; 44 | const sendContact: (to: string, contact: string | string[]) => any; 45 | const forwardMessages: ( 46 | to: string, 47 | messages: string | string[], 48 | skipMyMessages: boolean 49 | ) => any; 50 | const sendImageAsSticker: ( 51 | webpBase64: string, 52 | to: string, 53 | metadata?: any 54 | ) => void; 55 | const sendLocation: ( 56 | to: string, 57 | latitude: string, 58 | longitude: string, 59 | caption: string 60 | ) => void; 61 | const sendMessageMentioned: (...args: any) => any; 62 | const sendMessageToID: (id: string, message: string) => any; 63 | const setChatState: (chatState: string, chatId: string) => void; 64 | } 65 | 66 | export class SenderLayer extends ListenerLayer { 67 | constructor(public page: Page) { 68 | super(page); 69 | } 70 | 71 | /** 72 | * Sends a text message to given chat 73 | * @param to chat id: xxxxx@us.c 74 | * @param content text message 75 | */ 76 | public async sendText(to: string, content: string): Promise { 77 | return this.page.evaluate( 78 | ({ to, content }) => { 79 | WAPI.sendSeen(to); 80 | if (!WAPI.getChat(to)) { 81 | return WAPI.sendMessageToID(to, content); 82 | } else { 83 | return WAPI.sendMessage(to, content); 84 | } 85 | }, 86 | { to, content } 87 | ); 88 | } 89 | 90 | /** 91 | * Experimental! 92 | * Sends a text message to given chat even if its a non-existent chat 93 | * @param to chat id: xxxxx@us.c 94 | * @param content text message 95 | */ 96 | public async sendMessageToId(to: string, content: string): Promise { 97 | return this.page.evaluate( 98 | ({ to, content }) => { 99 | WAPI.sendSeen(to); 100 | return WAPI.sendMessageToID(to, content); 101 | }, 102 | { to, content } 103 | ); 104 | } 105 | 106 | /** 107 | * Sends image message 108 | * @param to Chat id 109 | * @param imgBase64 110 | * @param filename 111 | * @param caption 112 | */ 113 | public async sendImage( 114 | to: string, 115 | path: string, 116 | filename: string, 117 | caption?: string 118 | ) { 119 | const data = await fileToBase64(path); 120 | return this.page.evaluate( 121 | ({ to, data, filename, caption }) => { 122 | WAPI.sendImage(data, to, filename, caption); 123 | }, 124 | { to, data, filename, caption } 125 | ); 126 | } 127 | 128 | /** 129 | * Sends image message 130 | * @param to Chat id 131 | * @param imgBase64 132 | * @param filename 133 | * @param caption 134 | */ 135 | public async sendImageFromBase64( 136 | to: string, 137 | base64: string, 138 | filename: string, 139 | caption?: string 140 | ) { 141 | return this.page.evaluate( 142 | ({ to, base64, filename, caption }) => { 143 | WAPI.sendImage(base64, to, filename, caption); 144 | }, 145 | { to, base64, filename, caption } 146 | ); 147 | } 148 | 149 | /** 150 | * Sends message with thumbnail 151 | * @param thumb 152 | * @param url 153 | * @param title 154 | * @param description 155 | * @param chatId 156 | */ 157 | public async sendMessageWithThumb( 158 | thumb: string, 159 | url: string, 160 | title: string, 161 | description: string, 162 | chatId: string 163 | ) { 164 | return this.page.evaluate( 165 | ({ thumb, url, title, description, chatId }) => { 166 | WAPI.sendMessageWithThumb(thumb, url, title, description, chatId); 167 | }, 168 | { 169 | thumb, 170 | url, 171 | title, 172 | description, 173 | chatId, 174 | } 175 | ); 176 | } 177 | 178 | /** 179 | * Replies to given mesage id of given chat id 180 | * @param to Chat id 181 | * @param content Message body 182 | * @param quotedMsg Message id to reply to. 183 | */ 184 | public async reply(to: string, content: string, quotedMsg: string) { 185 | return await this.page.evaluate( 186 | ({ to, content, quotedMsg }) => { 187 | WAPI.reply(to, content, quotedMsg); 188 | }, 189 | { to, content, quotedMsg } 190 | ); 191 | } 192 | 193 | /** 194 | * Sends file 195 | * base64 parameter should have mime type already defined 196 | * @param to Chat id 197 | * @param base64 base64 data 198 | * @param filename 199 | * @param caption 200 | */ 201 | public async sendFileFromBase64( 202 | to: string, 203 | base64: string, 204 | filename: string, 205 | caption?: string 206 | ) { 207 | return this.page.evaluate( 208 | ({ to, base64, filename, caption }) => { 209 | WAPI.sendFile(base64, to, filename, caption); 210 | }, 211 | { to, base64, filename, caption } 212 | ); 213 | } 214 | 215 | /** 216 | * Sends file from path 217 | * @param to Chat id 218 | * @param path File path 219 | * @param filename 220 | * @param caption 221 | */ 222 | public async sendFile( 223 | to: string, 224 | path: string, 225 | filename: string, 226 | caption?: string 227 | ) { 228 | const base64 = await fileToBase64(path); 229 | return this.sendFileFromBase64(to, base64, filename, caption); 230 | } 231 | 232 | /** 233 | * Sends a video to given chat as a gif, with caption or not, using base64 234 | * @param to chat id xxxxx@us.c 235 | * @param base64 base64 data:video/xxx;base64,xxx 236 | * @param filename string xxxxx 237 | * @param caption string xxxxx 238 | */ 239 | public async sendVideoAsGif( 240 | to: string, 241 | path: string, 242 | filename: string, 243 | caption: string 244 | ) { 245 | const base64 = await fileToBase64(path); 246 | return this.sendVideoAsGifFromBase64(to, base64, filename, caption); 247 | } 248 | 249 | /** 250 | * Sends a video to given chat as a gif, with caption or not, using base64 251 | * @param to chat id xxxxx@us.c 252 | * @param base64 base64 data:video/xxx;base64,xxx 253 | * @param filename string xxxxx 254 | * @param caption string xxxxx 255 | */ 256 | public async sendVideoAsGifFromBase64( 257 | to: string, 258 | base64: string, 259 | filename: string, 260 | caption: string 261 | ) { 262 | return await this.page.evaluate( 263 | ({ to, base64, filename, caption }) => { 264 | WAPI.sendVideoAsGif(base64, to, filename, caption); 265 | }, 266 | { to, base64, filename, caption } 267 | ); 268 | } 269 | 270 | /** 271 | * Sends contact card to iven chat id 272 | * @param to Chat id 273 | * @param contactsId Example: 0000@c.us | [000@c.us, 1111@c.us] 274 | */ 275 | public async sendContact(to: string, contactsId: string | string[]) { 276 | return this.page.evaluate( 277 | ({ to, contactsId }) => WAPI.sendContact(to, contactsId), 278 | { to, contactsId } 279 | ); 280 | } 281 | 282 | /** 283 | * Forwards array of messages (could be ids or message objects) 284 | * @param to Chat id 285 | * @param messages Array of messages ids to be forwarded 286 | * @param skipMyMessages 287 | */ 288 | public async forwardMessages( 289 | to: string, 290 | messages: string | string[], 291 | skipMyMessages: boolean 292 | ) { 293 | return this.page.evaluate( 294 | ({ to, messages, skipMyMessages }) => 295 | WAPI.forwardMessages(to, messages, skipMyMessages), 296 | { to, messages, skipMyMessages } 297 | ); 298 | } 299 | 300 | /** 301 | * Generates sticker from given image and sends it 302 | * @param path image path 303 | * @param to 304 | */ 305 | public async sendImageAsSticker(to: string, path: string) { 306 | const b64 = await fileToBase64(path); 307 | const buff = Buffer.from( 308 | b64.replace(/^data:image\/(png|gif|jpeg);base64,/, ''), 309 | 'base64' 310 | ); 311 | const mimeInfo = base64MimeType(b64); 312 | if (!mimeInfo || mimeInfo.includes('image')) { 313 | // Convert to webp, resize + autoscale to width 512 px 314 | const scaledImageBuffer = await sharp(buff, { failOnError: false }) 315 | .resize({ width: 512, height: 512 }) 316 | .toBuffer(); 317 | const webp = sharp(scaledImageBuffer, { failOnError: false }).webp(); 318 | const metadata = (await webp.metadata()) as any; 319 | const webpBase64 = (await webp.toBuffer()).toString('base64'); 320 | return await this.page.evaluate( 321 | ({ webpBase64, to, metadata }) => 322 | WAPI.sendImageAsSticker(webpBase64, to, metadata), 323 | { webpBase64, to, metadata } 324 | ); 325 | } else { 326 | console.log('Not an image'); 327 | return false; 328 | } 329 | } 330 | 331 | /** 332 | * TODO: Fix message not being delivered 333 | * Sends location to given chat id 334 | * @param to Chat id 335 | * @param latitude Latitude 336 | * @param longitude Longitude 337 | * @param caption Text caption 338 | */ 339 | public async sendLocation( 340 | to: string, 341 | latitude: number, 342 | longitude: number, 343 | title?: string, 344 | subtitle?: string 345 | ) { 346 | // Create caption 347 | let caption = title || ''; 348 | if (subtitle) { 349 | caption = `${title}\n${subtitle}`; 350 | } 351 | 352 | return await this.page.evaluate( 353 | ({ to, latitude, longitude, caption }) => { 354 | WAPI.sendLocation(to, latitude, longitude, caption); 355 | }, 356 | { to, latitude, longitude, caption } 357 | ); 358 | } 359 | 360 | /** 361 | * Sets a chat status to seen. Marks all messages as ack: 3 362 | * @param chatId chat id: xxxxx@us.c 363 | */ 364 | public async sendSeen(chatId: string) { 365 | return this.page.evaluate((chatId) => WAPI.sendSeen(chatId), chatId); 366 | } 367 | 368 | /** 369 | * Starts typing ('Typing...' state) 370 | * @param chatId 371 | */ 372 | public async startTyping(to: string) { 373 | return this.page.evaluate(({ to }) => WAPI.startTyping(to), { to }); 374 | } 375 | 376 | /** 377 | * Stops typing 378 | * @param to Chat id 379 | */ 380 | public async stopTyping(to: string) { 381 | return this.page.evaluate(({ to }) => WAPI.stopTyping(to), { to }); 382 | } 383 | 384 | /** 385 | * Sends text with tags 386 | * 387 | */ 388 | public async sendMentioned(to: string, message: string, mentioned: string[]) { 389 | return await this.page.evaluate( 390 | ({ to, message, mentioned }) => { 391 | WAPI.sendMessageMentioned(to, message, mentioned); 392 | }, 393 | { to, message, mentioned } 394 | ); 395 | } 396 | 397 | /** 398 | * Sets the chat state 399 | * @param chatState 400 | * @param chatId 401 | */ 402 | public async setChatState(chatId: string, chatState: ChatState) { 403 | return await this.page.evaluate( 404 | ({ chatState, chatId }) => { 405 | WAPI.setChatState(chatState, chatId); 406 | }, 407 | { chatState: chatState, chatId } 408 | ); 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/api/layers/ui.layer.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { GroupLayer } from './group.layer'; 3 | 4 | declare module WAPI { 5 | const openChat: (chatId: string) => boolean; 6 | const openChatAt: ( 7 | chatId: string, 8 | messageId: string 9 | ) => { wasVisible: boolean; alignAt: string }; 10 | } 11 | 12 | export class UILayer extends GroupLayer { 13 | constructor(page: Page) { 14 | super(page); 15 | } 16 | 17 | /** 18 | * Opens given chat at last message (bottom) 19 | * Will fire natural workflow events of whatsapp web 20 | * @param chatId 21 | */ 22 | public async openChat(chatId: string) { 23 | return this.page.evaluate( 24 | (chatId: string) => WAPI.openChat(chatId), 25 | chatId 26 | ); 27 | } 28 | 29 | /** 30 | * Opens chat at given message position 31 | * @param chatId Chat id 32 | * @param messageId Message id (For example: '06D3AB3D0EEB9D077A3F9A3EFF4DD030') 33 | */ 34 | public async openChatAt(chatId: string, messageId: string) { 35 | return this.page.evaluate( 36 | (chatId: string) => WAPI.openChatAt(chatId, messageId), 37 | chatId 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/api/model/ack.ts: -------------------------------------------------------------------------------- 1 | import { Id } from './id'; 2 | import { AckType } from './enum'; 3 | 4 | export interface Ack { 5 | id: Id; 6 | body: string; 7 | type: string; 8 | t: number; 9 | subtype: any; 10 | notifyName: string; 11 | from: string; 12 | to: string; 13 | self: string; 14 | ack: AckType; 15 | invis: boolean; 16 | isNewMsg: boolean; 17 | star: boolean; 18 | loc: string; 19 | lat: number; 20 | lng: number; 21 | mentionedJidList: any[]; 22 | isForwarded: boolean; 23 | labels: any[]; 24 | ephemeralStartTimestamp: number; 25 | } 26 | -------------------------------------------------------------------------------- /src/api/model/chat.ts: -------------------------------------------------------------------------------- 1 | import { Contact } from './contact'; 2 | import { GroupMetadata } from './group-metadata'; 3 | import { Id } from './id'; 4 | 5 | export interface Chat { 6 | id: Id; 7 | pendingMsgs: boolean; 8 | lastReceivedKey: LastReceivedKey; 9 | t: number; 10 | unreadCount: number; 11 | archive: boolean; 12 | isReadOnly: boolean; 13 | muteExpiration: number; 14 | name: string; 15 | notSpam: boolean; 16 | pin: number; 17 | msgs: null; 18 | kind: string; 19 | isGroup: boolean; 20 | contact: Contact; 21 | groupMetadata: GroupMetadata; 22 | presence: Presence; 23 | isOnline: null; 24 | lastSeen: null; 25 | } 26 | 27 | export interface ProfilePicThumbObj { 28 | eurl: string; 29 | id: Id; 30 | img: string; 31 | imgFull: string; 32 | raw: null; 33 | tag: string; 34 | } 35 | 36 | export interface LastReceivedKey { 37 | fromMe: boolean; 38 | remote: Id; 39 | id: string; 40 | _serialized: string; 41 | } 42 | 43 | export interface Presence { 44 | id: Id; 45 | chatstates: any[]; 46 | } 47 | -------------------------------------------------------------------------------- /src/api/model/contact-status.ts: -------------------------------------------------------------------------------- 1 | export interface ContactStatus { 2 | id: string; 3 | status: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/api/model/contact.ts: -------------------------------------------------------------------------------- 1 | import { Id } from './id'; 2 | 3 | export interface Contact { 4 | formattedName: string; 5 | id: Id; 6 | isBusiness: boolean; 7 | isEnterprise: boolean; 8 | isHighLevelVerified: any; 9 | isMe: boolean; 10 | isMyContact: boolean; 11 | isPSA: boolean; 12 | isUser: boolean; 13 | isVerified: any; 14 | isWAContact: boolean; 15 | labels: any[]; 16 | msgs: any; 17 | name: string; 18 | plaintextDisabled: boolean; 19 | profilePicThumbObj: { 20 | eurl: string; 21 | id: Id; 22 | img: string; 23 | imgFull: string; 24 | raw: any; 25 | tag: string; 26 | }; 27 | pushname: string; 28 | sectionHeader: any; 29 | shortName: string; 30 | statusMute: boolean; 31 | type: string; 32 | verifiedLevel: any; 33 | verifiedName: any; 34 | isOnline?: any; 35 | lastSeen?: any; 36 | } 37 | -------------------------------------------------------------------------------- /src/api/model/enum/ack-type.ts: -------------------------------------------------------------------------------- 1 | export enum AckType { 2 | MD_DOWNGRADE = -7, 3 | INACTIVE = -6, 4 | CONTENT_UNUPLOADABLE = -5, 5 | CONTENT_TOO_BIG = -4, 6 | CONTENT_GONE = -3, 7 | EXPIRED = -2, 8 | FAILED = -1, 9 | CLOCK = 0, 10 | SENT = 1, 11 | RECEIVED = 2, 12 | READ = 3, 13 | PLAYED = 4, 14 | } 15 | -------------------------------------------------------------------------------- /src/api/model/enum/chat-state.ts: -------------------------------------------------------------------------------- 1 | export enum ChatState { 2 | Typing = 0, 3 | Recording = 1, 4 | Paused = 2, 5 | } 6 | -------------------------------------------------------------------------------- /src/api/model/enum/group-change-event.ts: -------------------------------------------------------------------------------- 1 | export enum GroupChangeEvent { 2 | Remove = 'remove', 3 | Add = 'add', 4 | } 5 | -------------------------------------------------------------------------------- /src/api/model/enum/group-notification-type.ts: -------------------------------------------------------------------------------- 1 | export enum GroupNotificationType { 2 | Add = 'add', 3 | Inivite = 'invite', 4 | Remove = 'remove', 5 | Leave = 'leave', 6 | Subject = 'subject', 7 | Description = 'description', 8 | Picture = 'picture', 9 | Announce = 'announce', 10 | Restrict = 'restrict', 11 | } 12 | -------------------------------------------------------------------------------- /src/api/model/enum/index.ts: -------------------------------------------------------------------------------- 1 | export { AckType } from './ack-type'; 2 | export { ChatState } from './chat-state'; 3 | export { GroupChangeEvent } from './group-change-event'; 4 | export { GroupNotificationType } from './group-notification-type'; 5 | export { SocketState } from './socket-state'; 6 | export { MessageType, MediaType } from './message-type'; 7 | -------------------------------------------------------------------------------- /src/api/model/enum/message-type.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | TEXT = 'chat', 3 | AUDIO = 'audio', 4 | VOICE = 'ptt', 5 | IMAGE = 'image', 6 | VIDEO = 'video', 7 | DOCUMENT = 'document', 8 | STICKER = 'sticker', 9 | LOCATION = 'location', 10 | CONTACT_CARD = 'vcard', 11 | CONTACT_CARD_MULTI = 'multi_vcard', 12 | REVOKED = 'revoked', 13 | UNKNOWN = 'unknown', 14 | } 15 | 16 | export enum MediaType { 17 | IMAGE = 'Image', 18 | VIDEO = 'Video', 19 | AUDIO = 'Audio', 20 | PTT = 'Audio', 21 | DOCUMENT = 'Document', 22 | STICKER = 'Image', 23 | } 24 | -------------------------------------------------------------------------------- /src/api/model/enum/socket-state.ts: -------------------------------------------------------------------------------- 1 | export enum SocketState { 2 | OPENING = 'OPENING', 3 | PAIRING = 'PAIRING', 4 | UNPAIRED = 'UNPAIRED', 5 | UNPAIRED_IDLE = 'UNPAIRED_IDLE', 6 | CONNECTED = 'CONNECTED', 7 | TIMEOUT = 'TIMEOUT', 8 | CONFLICT = 'CONFLICT', 9 | UNLAUNCHED = 'UNLAUNCHED', 10 | PROXYBLOCK = 'PROXYBLOCK', 11 | TOS_BLOCK = 'TOS_BLOCK', 12 | SMB_TOS_BLOCK = 'SMB_TOS_BLOCK', 13 | DEPRECATED_VERSION = 'DEPRECATED_VERSION', 14 | } 15 | -------------------------------------------------------------------------------- /src/api/model/group-creation.ts: -------------------------------------------------------------------------------- 1 | export interface GroupCreation { 2 | status: number; 3 | gid: { 4 | server: string; 5 | user: string; 6 | _serialized: string; 7 | }; 8 | participants: { [key: string]: any[] }[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/api/model/group-metadata.ts: -------------------------------------------------------------------------------- 1 | import { Id } from '.'; 2 | 3 | export interface GroupMetadata { 4 | id: Id; 5 | creation: number; 6 | owner: { 7 | server: string; 8 | user: string; 9 | _serialized: string; 10 | }; 11 | participants: any[]; 12 | pendingParticipants: any[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/api/model/host-device.ts: -------------------------------------------------------------------------------- 1 | export interface HostDevice { 2 | id: string; 3 | ref: string; 4 | refTTL: number; 5 | wid: Me; 6 | connected: boolean; 7 | me: Me; 8 | protoVersion: number[]; 9 | clientToken: string; 10 | serverToken: string; 11 | isResponse: string; 12 | battery: number; 13 | plugged: boolean; 14 | lc: string; 15 | lg: string; 16 | locales: string; 17 | is24h: boolean; 18 | platform: string; 19 | phone: Phone; 20 | tos: number; 21 | smbTos: number; 22 | pushname: string; 23 | blockStoreAdds: boolean; 24 | } 25 | 26 | export interface Me { 27 | server: string; 28 | user: string; 29 | _serialized: string; 30 | } 31 | 32 | export interface Phone { 33 | wa_version: string; 34 | mcc: string; 35 | mnc: string; 36 | os_version: string; 37 | device_manufacturer: string; 38 | device_model: string; 39 | os_build_number: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/api/model/id.ts: -------------------------------------------------------------------------------- 1 | export interface Id { 2 | server: string; 3 | user: string; 4 | _serialized: string; 5 | fromMe: boolean; 6 | remote: string; 7 | id: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/api/model/index.ts: -------------------------------------------------------------------------------- 1 | export { Chat } from './chat'; 2 | export { Contact } from './contact'; 3 | export { GroupMetadata } from './group-metadata'; 4 | export { Id } from './id'; 5 | export { Message } from './message'; 6 | export { ParticipantEvent } from './participant-event'; 7 | export { PartialMessage } from './partial-message'; 8 | export { Ack } from './ack'; 9 | export { HostDevice } from './host-device'; 10 | export { LiveLocation } from './live-location'; 11 | export { ContactStatus } from './contact-status'; 12 | export { GroupCreation } from './group-creation'; 13 | export { WhatsappProfile } from './whatsapp-profile'; 14 | -------------------------------------------------------------------------------- /src/api/model/live-location.ts: -------------------------------------------------------------------------------- 1 | export interface LiveLocation { 2 | id: string; 3 | lat: number; 4 | lng: number; 5 | speed: number; 6 | lastUpdated: number; 7 | accuracy: number; 8 | degrees: any; 9 | } 10 | -------------------------------------------------------------------------------- /src/api/model/message.ts: -------------------------------------------------------------------------------- 1 | export interface Message { 2 | id: string; 3 | body: string; 4 | type: string; 5 | t: number; 6 | notifyName: string; 7 | from: string; 8 | to: string; 9 | author: string; 10 | self: string; 11 | ack: number; 12 | invis: boolean; 13 | isNewMsg: boolean; 14 | star: boolean; 15 | recvFresh: boolean; 16 | interactiveAnnotations: any[]; 17 | clientUrl: string; 18 | directPath: string; 19 | mimetype: string; 20 | filehash: string; 21 | uploadhash: string; 22 | size: number; 23 | mediaKey: string; 24 | mediaKeyTimestamp: number; 25 | width: number; 26 | height: number; 27 | broadcast: boolean; 28 | mentionedJidList: any[]; 29 | isForwarded: boolean; 30 | labels: any[]; 31 | sender: Sender; 32 | timestamp: number; 33 | content: string; 34 | isGroupMsg: boolean; 35 | isMMS: boolean; 36 | isMedia: boolean; 37 | isNotification: boolean; 38 | isPSA: boolean; 39 | chat: { 40 | id: string; 41 | pendingMsgs: boolean; 42 | lastReceivedKey: LastReceivedKey; 43 | t: number; 44 | unreadCount: number; 45 | archive: boolean; 46 | isReadOnly: boolean; 47 | muteExpiration: number; 48 | name: string; 49 | notSpam: boolean; 50 | pin: number; 51 | msgs: null; 52 | kind: string; 53 | isGroup: boolean; 54 | contact: Sender; 55 | groupMetadata: null; 56 | presence: Presence; 57 | isOnline: null; 58 | lastSeen: null; 59 | }; 60 | isOnline: null; 61 | lastSeen: null; 62 | chatId: string; 63 | quotedMsgObj: null; 64 | mediaData: MediaData; 65 | } 66 | 67 | export interface Sender { 68 | id: string; 69 | name: string; 70 | shortName: string; 71 | pushname: string; 72 | type: string; 73 | isBusiness: boolean; 74 | isEnterprise: boolean; 75 | statusMute: boolean; 76 | labels: any[]; 77 | formattedName: string; 78 | isMe: boolean; 79 | isMyContact: boolean; 80 | isPSA: boolean; 81 | isUser: boolean; 82 | isWAContact: boolean; 83 | profilePicThumbObj: ProfilePicThumbObj; 84 | msgs: null; 85 | } 86 | 87 | export interface ProfilePicThumbObj { 88 | eurl: string; 89 | id: string; 90 | img: string; 91 | imgFull: string; 92 | raw: null; 93 | tag: string; 94 | } 95 | 96 | export interface LastReceivedKey { 97 | fromMe: boolean; 98 | remote: string; 99 | id: string; 100 | _serialized: string; 101 | } 102 | 103 | export interface Presence { 104 | id: string; 105 | chatstates: any[]; 106 | } 107 | 108 | export interface MediaData { 109 | type: string; 110 | mediaStage: string; 111 | animationDuration: number; 112 | animatedAsNewMsg: boolean; 113 | _swStreamingSupported: boolean; 114 | _listeningToSwSupport: boolean; 115 | } 116 | -------------------------------------------------------------------------------- /src/api/model/partial-message.ts: -------------------------------------------------------------------------------- 1 | export interface PartialMessage { 2 | id: ID; 3 | body: string; 4 | type: string; 5 | t: number; 6 | notifyName: string; 7 | from: string; 8 | to: string; 9 | self: string; 10 | ack: number; 11 | invis: boolean; 12 | star: boolean; 13 | broadcast: boolean; 14 | mentionedJidList: any[]; 15 | isForwarded: boolean; 16 | labels: any[]; 17 | } 18 | 19 | interface ID { 20 | fromMe: boolean; 21 | remote: string; 22 | id: string; 23 | _serialized: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/api/model/participant-event.ts: -------------------------------------------------------------------------------- 1 | import { Id } from './id'; 2 | import { GroupChangeEvent } from './enum'; 3 | 4 | export interface ParticipantEvent { 5 | by: Id; 6 | action: GroupChangeEvent; 7 | who: [Id]; 8 | } 9 | -------------------------------------------------------------------------------- /src/api/model/whatsapp-profile.ts: -------------------------------------------------------------------------------- 1 | import { Id } from './id'; 2 | 3 | export interface WhatsappProfile { 4 | id: Id; 5 | status: number; 6 | isBusiness: boolean; 7 | canReceiveMessage: boolean; 8 | numberExists: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/api/whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { decrypt } from './helpers/decrypt'; 3 | import { ControlsLayer } from './layers/controls.layer'; 4 | import { Message } from './model'; 5 | 6 | declare module WAPI { 7 | const arrayBufferToBase64: (buffer: ArrayBuffer) => string; 8 | } 9 | 10 | export class Whatsapp extends ControlsLayer { 11 | constructor(public page: Page) { 12 | super(page); 13 | } 14 | 15 | /** 16 | * Get the puppeteer page instance 17 | * @returns The Whatsapp page 18 | */ 19 | get waPage(): Page { 20 | return this.page; 21 | } 22 | 23 | /** 24 | * Clicks on 'use here' button (When it get unlaunched) 25 | * This method tracks the class of the button 26 | * Whatsapp web might change this class name over the time 27 | * Dont rely on this method 28 | */ 29 | public async useHere() { 30 | await this.page.waitForFunction( 31 | () => { 32 | const useHereClass = '._1WZqU.PNlAR'; 33 | return document.querySelector(useHereClass); 34 | }, 35 | { timeout: 0 } 36 | ); 37 | 38 | await this.page.evaluate(() => { 39 | const useHereClass = '._1WZqU.PNlAR'; 40 | (document.querySelector(useHereClass)).click(); 41 | }); 42 | } 43 | 44 | /** 45 | * Closes page and browser 46 | */ 47 | public async close() { 48 | if (this.page) { 49 | await this.page.close(); 50 | } 51 | 52 | if (this.page.browser) { 53 | await this.page.browser().close(); 54 | } 55 | } 56 | 57 | /** 58 | * Decrypts message file 59 | * @param message Message object 60 | * @returns Decrypted file buffer (null otherwise) 61 | */ 62 | public async downloadFile(message: Message) { 63 | if (message.isMedia) { 64 | const url = message.clientUrl; 65 | const encBase64 = await this.page.evaluate((url: string) => { 66 | return fetch(url) 67 | .then((response) => response.arrayBuffer()) 68 | .then((bytes) => WAPI.arrayBufferToBase64(bytes)); 69 | }, url); 70 | 71 | return decrypt(encBase64, message); 72 | } else { 73 | return null; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/config/create-config.ts: -------------------------------------------------------------------------------- 1 | export interface CreateConfig { 2 | headless?: boolean; 3 | devtools?: boolean; 4 | useChrome?: boolean; 5 | debug?: boolean; 6 | browserArgs?: string[]; 7 | logQR?: boolean; 8 | refreshQR?: number; 9 | } 10 | 11 | export const defaultOptions: CreateConfig = { 12 | headless: true, 13 | devtools: false, 14 | useChrome: true, 15 | debug: false, 16 | logQR: true, 17 | browserArgs: [''], 18 | refreshQR: 30000, 19 | }; 20 | -------------------------------------------------------------------------------- /src/config/puppeteer.config.ts: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | const puppeteerConfig = { 4 | whatsappUrl: 'https://web.whatsapp.com', 5 | chroniumArgs: [ 6 | // `--app=${whatsappUrl}`, 7 | '--log-level=3', // fatal only 8 | //'--start-maximized', 9 | '--no-default-browser-check', 10 | '--disable-site-isolation-trials', 11 | '--no-experiments', 12 | '--ignore-gpu-blacklist', 13 | '--ignore-certificate-errors', 14 | '--ignore-certificate-errors-spki-list', 15 | '--disable-gpu', 16 | '--disable-extensions', 17 | '--disable-default-apps', 18 | '--enable-features=NetworkService', 19 | '--disable-setuid-sandbox', 20 | '--no-sandbox', 21 | // Extras 22 | '--disable-webgl', 23 | '--disable-threaded-animation', 24 | '--disable-threaded-scrolling', 25 | '--disable-in-process-stack-traces', 26 | '--disable-histogram-customizer', 27 | '--disable-gl-extensions', 28 | '--disable-composited-antialiasing', 29 | '--disable-canvas-aa', 30 | '--disable-3d-apis', 31 | '--disable-accelerated-2d-canvas', 32 | '--disable-accelerated-jpeg-decoding', 33 | '--disable-accelerated-mjpeg-decode', 34 | '--disable-app-list-dismiss-on-blur', 35 | '--disable-accelerated-video-decode' 36 | ] 37 | }; 38 | 39 | export { puppeteerConfig }; 40 | -------------------------------------------------------------------------------- /src/controllers/auth.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { Page } from 'puppeteer'; 3 | import * as qrcode from 'qrcode-terminal'; 4 | import { BehaviorSubject, from, merge, of, race } from 'rxjs'; 5 | import { filter, take } from 'rxjs/operators'; 6 | import { htmlAuthQuery } from './constants/html-auth-query'; 7 | 8 | const networkAuthenticated = new BehaviorSubject(false); 9 | const htmlAuthenticated = (page: Page) => { 10 | return from( 11 | page 12 | .waitForFunction(htmlAuthQuery, { 13 | timeout: 0, 14 | }) 15 | .then(() => true) 16 | ); 17 | }; 18 | 19 | /** 20 | * Detects login via network requests listening 21 | * @param waPage 22 | */ 23 | export const listenNetworkAuth = (waPage: Page) => { 24 | waPage.on('response', async function callback(response) { 25 | if (response.url().includes('_priority_components')) { 26 | networkAuthenticated.next(true); 27 | waPage.removeListener('response', callback); 28 | } 29 | }); 30 | }; 31 | 32 | /** 33 | * Validates if client is authenticated 34 | * @returns true if is authenticated, false otherwise 35 | * @param waPage 36 | */ 37 | export const isAuthenticated = (waPage: Page) => { 38 | return merge(needsToScan(waPage), isInsideChat(waPage)) 39 | .pipe(take(1)) 40 | .toPromise(); 41 | }; 42 | 43 | export const needsToScan = (waPage: Page) => { 44 | return from( 45 | waPage 46 | .waitForSelector('body > div > div > .landing-wrapper', { 47 | timeout: 0, 48 | }) 49 | .then(() => false) 50 | ); 51 | }; 52 | 53 | export const isInsideChat = (waPage: Page) => { 54 | if (networkAuthenticated.getValue()) { 55 | return of(true); 56 | } 57 | 58 | return race([ 59 | networkAuthenticated.pipe(filter((auth) => auth)), 60 | htmlAuthenticated(waPage), 61 | ]); 62 | }; 63 | 64 | export async function retrieveQR(page: Page) { 65 | const { code, data } = await decodeQR(page); 66 | const asciiQR = await asciiQr(code); 67 | return { code, data, asciiQR }; 68 | } 69 | 70 | async function asciiQr(code: string): Promise { 71 | return new Promise((resolve, reject) => { 72 | qrcode.generate(code, { small: true }, (qrcode) => { 73 | resolve(qrcode); 74 | }); 75 | }); 76 | } 77 | 78 | async function decodeQR(page: Page): Promise<{ code: string; data: string }> { 79 | await page.waitForSelector('canvas', { timeout: 0 }); 80 | await page.addScriptTag({ 81 | path: require.resolve(path.join(__dirname, '../lib/jsQR', 'jsQR.js')), 82 | }); 83 | 84 | return await page.evaluate(() => { 85 | const canvas = document.querySelector('canvas'); 86 | const context = canvas.getContext('2d'); 87 | 88 | // @ts-ignore 89 | const code = jsQR( 90 | context.getImageData(0, 0, canvas.width, canvas.height).data, 91 | canvas.width, 92 | canvas.height 93 | ); 94 | 95 | return { code: code.data, data: canvas.toDataURL() }; 96 | }); 97 | } 98 | -------------------------------------------------------------------------------- /src/controllers/browser.ts: -------------------------------------------------------------------------------- 1 | import * as ChromeLauncher from 'chrome-launcher'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { Browser, Page } from 'puppeteer'; 5 | import puppeteer from 'puppeteer-extra'; 6 | import { CreateConfig } from '../config/create-config'; 7 | import { puppeteerConfig } from '../config/puppeteer.config'; 8 | import { listenNetworkAuth } from './auth'; 9 | import StealthPlugin = require('puppeteer-extra-plugin-stealth'); 10 | import { sleep } from '../utils/sleep'; 11 | 12 | export async function initWhatsapp(session: string, options: CreateConfig) { 13 | const browser = await initBrowser(session, options); 14 | const waPage = await getWhatsappPage(browser); 15 | listenNetworkAuth(waPage); 16 | 17 | await waPage.setUserAgent( 18 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36' 19 | ); 20 | 21 | await waPage.goto(puppeteerConfig.whatsappUrl); 22 | return waPage; 23 | } 24 | 25 | export async function injectApi(page: Page) { 26 | await page.waitForFunction(() => { 27 | // @ts-ignore 28 | return webpackJsonp != undefined; 29 | }); 30 | 31 | // const wapi = fs.readFileSync( 32 | // path.join(__dirname, '../lib/wapi', 'wapi.js'), 33 | // 'utf8' 34 | // ); 35 | // await page.addScriptTag({ content: wapi }); 36 | // await page.evaluate(wapi); 37 | 38 | // const middleware = fs.readFileSync( 39 | // path.join(__dirname, '../lib/middleware', 'middleware.js'), 40 | // 'utf8' 41 | // ); 42 | // await page.addScriptTag({ content: middleware }); 43 | 44 | await page.addScriptTag({ 45 | path: require.resolve(path.join(__dirname, '../lib/wapi', 'wapi.js')), 46 | }); 47 | 48 | await page.addScriptTag({ 49 | path: require.resolve( 50 | path.join(__dirname, '../lib/middleware', 'middleware.js') 51 | ), 52 | }); 53 | 54 | // Make sure WAPI is initialized 55 | await page.waitForFunction(() => { 56 | // @ts-ignore 57 | return !!WAPI.getWAVersion; 58 | }); 59 | 60 | return page; 61 | } 62 | 63 | /** 64 | * Initializes browser, will try to use chrome as default 65 | * @param session 66 | */ 67 | async function initBrowser( 68 | session: string, 69 | options: CreateConfig, 70 | extras = {} 71 | ) { 72 | if (options.useChrome) { 73 | const chromePath = getChrome(); 74 | if (chromePath) { 75 | extras = { ...extras, executablePath: chromePath }; 76 | } else { 77 | console.log('Chrome not found, using chromium'); 78 | extras = {}; 79 | } 80 | } 81 | 82 | // Use stealth plugin to avoid being detected as a bot 83 | puppeteer.use(StealthPlugin()); 84 | 85 | const browser = await puppeteer.launch({ 86 | // headless: true, 87 | headless: options.headless, 88 | devtools: options.devtools, 89 | userDataDir: path.join(process.cwd(), session), 90 | args: options.browserArgs 91 | ? options.browserArgs 92 | : [...puppeteerConfig.chroniumArgs], 93 | ...extras, 94 | }); 95 | return browser; 96 | } 97 | 98 | async function getWhatsappPage(browser: Browser) { 99 | const pages = await browser.pages(); 100 | console.assert(pages.length > 0); 101 | return pages[0]; 102 | } 103 | 104 | /** 105 | * Retrieves chrome instance path 106 | */ 107 | function getChrome() { 108 | try { 109 | const chromeInstalations = ChromeLauncher.Launcher.getInstallations(); 110 | return chromeInstalations[0]; 111 | } catch (error) { 112 | return undefined; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/controllers/constants/html-auth-query.ts: -------------------------------------------------------------------------------- 1 | export const htmlAuthQuery = `(document.getElementsByClassName('app')[0] && 2 | document.getElementsByClassName('app')[0].attributes && 3 | !!document.getElementsByClassName('app')[0].attributes.tabindex) || 4 | (document.getElementsByClassName('two')[0] && 5 | document.getElementsByClassName('two')[0].attributes && 6 | !!document.getElementsByClassName('two')[0].attributes.tabindex) 7 | `; 8 | -------------------------------------------------------------------------------- /src/controllers/initializer.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import latestVersion from 'latest-version'; 3 | import { Page } from 'puppeteer'; 4 | import { timer } from 'rxjs'; 5 | import { switchMap, takeUntil } from 'rxjs/operators'; 6 | import { Whatsapp } from '../api/whatsapp'; 7 | import { CreateConfig, defaultOptions } from '../config/create-config'; 8 | import { upToDate } from '../utils/semver'; 9 | import { isAuthenticated, isInsideChat, retrieveQR } from './auth'; 10 | import { initWhatsapp, injectApi } from './browser'; 11 | 12 | import Spinnies = require('spinnies'); 13 | import boxen = require('boxen'); 14 | import chalk = require('chalk'); 15 | const { version } = require('../../package.json'); 16 | 17 | // Global 18 | let updatesChecked = false; 19 | 20 | /** 21 | * Should be called to initialize whatsapp client 22 | */ 23 | export async function create( 24 | session = 'session', 25 | catchQR?: (qrCode: string, asciiQR: string) => void, 26 | options?: CreateConfig 27 | ) { 28 | const spinnies = new Spinnies(); 29 | 30 | // Check for updates if needed 31 | if (!updatesChecked) { 32 | spinnies.add('sulla-version-spinner', { text: 'Checking for updates...' }); 33 | checkSullaVersion(spinnies); 34 | updatesChecked = true; 35 | } 36 | 37 | // Initialize whatsapp 38 | spinnies.add(`${session}-auth`, { text: 'Creating whatsapp instance...' }); 39 | 40 | const mergedOptions = { ...defaultOptions, ...options }; 41 | let waPage = await initWhatsapp(session, mergedOptions); 42 | 43 | spinnies.update(`${session}-auth`, { text: 'Authenticating...' }); 44 | const authenticated = await isAuthenticated(waPage); 45 | 46 | // If not authenticated, show QR and wait for scan 47 | if (authenticated) { 48 | // Wait til inside chat 49 | await isInsideChat(waPage).toPromise(); 50 | spinnies.succeed(`${session}-auth`, { text: 'Authenticated' }); 51 | } else { 52 | spinnies.update(`${session}-auth`, { 53 | text: `Authenticate to continue`, 54 | }); 55 | 56 | if (mergedOptions.refreshQR <= 0) { 57 | const { data, asciiQR } = await retrieveQR(waPage); 58 | if (catchQR) { 59 | catchQR(data, asciiQR); 60 | } 61 | 62 | if (mergedOptions.logQR) { 63 | console.log(`Scan QR for: ${session}`); 64 | console.log(asciiQR); 65 | } 66 | } else { 67 | grabQRUntilInside(waPage, mergedOptions, session, catchQR); 68 | } 69 | 70 | // Wait til inside chat 71 | await isInsideChat(waPage).toPromise(); 72 | spinnies.succeed(`${session}-auth`, { text: 'Authenticated' }); 73 | } 74 | 75 | spinnies.add(`${session}-inject`, { text: 'Injecting api...' }); 76 | waPage = await injectApi(waPage); 77 | spinnies.succeed(`${session}-inject`, { text: 'Injecting api' }); 78 | 79 | if (mergedOptions.debug) { 80 | const debugURL = `http://localhost:${readFileSync( 81 | `./${session}/DevToolsActivePort` 82 | ).slice(0, -54)}`; 83 | console.log(`\nDebug: \x1b[34m${debugURL}\x1b[0m`); 84 | } 85 | 86 | return new Whatsapp(waPage); 87 | } 88 | 89 | function grabQRUntilInside( 90 | waPage: Page, 91 | options: CreateConfig, 92 | session: string, 93 | catchQR: (qrCode: string, asciiQR: string) => void 94 | ) { 95 | const isInside = isInsideChat(waPage); 96 | timer(0, options.refreshQR) 97 | .pipe( 98 | takeUntil(isInside), 99 | switchMap(() => retrieveQR(waPage)) 100 | ) 101 | .subscribe(({ data, asciiQR }) => { 102 | if (catchQR) { 103 | catchQR(data, asciiQR); 104 | } 105 | if (options.logQR) { 106 | console.clear(); 107 | console.log(`Scan QR for: ${session}`); 108 | console.log(asciiQR); 109 | } 110 | }); 111 | } 112 | 113 | /** 114 | * Checs for a new versoin of sulla and logs 115 | */ 116 | function checkSullaVersion(spinnies) { 117 | latestVersion('sulla').then((latest) => { 118 | if (!upToDate(version, latest)) { 119 | logUpdateAvailable(version, latest); 120 | } 121 | 122 | spinnies.succeed('sulla-version-spinner', { text: 'Checking for updates' }); 123 | }); 124 | } 125 | 126 | /** 127 | * Logs a boxen of instructions to update 128 | * @param current 129 | * @param latest 130 | */ 131 | function logUpdateAvailable(current: string, latest: string) { 132 | // prettier-ignore 133 | const newVersionLog = 134 | `There is a new version of ${chalk.bold(`sulla`)} ${chalk.gray(current)} ➜ ${chalk.bold.green(latest)}\n` + 135 | `Update your package by running:\n\n` + 136 | `${chalk.bold('\>')} ${chalk.blueBright('npm update sulla')}`; 137 | 138 | console.log(boxen(newVersionLog, { padding: 1 })); 139 | console.log( 140 | `For more info visit: ${chalk.underline( 141 | 'https://github.com/danielcardeenas/sulla/blob/master/UPDATES.md' 142 | )}\n` 143 | ); 144 | } 145 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Ack, 3 | Chat, 4 | Contact, 5 | ContactStatus, 6 | GroupCreation, 7 | GroupMetadata, 8 | HostDevice, 9 | Id, 10 | LiveLocation, 11 | Message, 12 | PartialMessage, 13 | ParticipantEvent, 14 | WhatsappProfile, 15 | } from './api/model'; 16 | export { 17 | AckType, 18 | ChatState, 19 | GroupChangeEvent, 20 | GroupNotificationType, 21 | MessageType, 22 | SocketState, 23 | } from './api/model/enum'; 24 | export { Whatsapp } from './api/whatsapp'; 25 | export { create } from './controllers/initializer'; 26 | -------------------------------------------------------------------------------- /src/lib/README.md: -------------------------------------------------------------------------------- 1 | ## Files here should not be touched. 2 | 3 | --- 4 | 5 | Files inside this directory are automatically generated and copied into dist bundle with the build scripts 6 | -------------------------------------------------------------------------------- /src/lib/jsQR/README.md: -------------------------------------------------------------------------------- 1 | This file is already bundled and minified. No need for building or webpack here -------------------------------------------------------------------------------- /src/lib/jsQR/gulpfile.js: -------------------------------------------------------------------------------- 1 | const { src, dest } = require('gulp'); 2 | const path = require('path'); 3 | 4 | function copy() { 5 | return src('./jsQR.js') 6 | .pipe(dest(path.resolve(__dirname, '../../../dist/lib/jsQR'))); 7 | } 8 | 9 | exports.default = copy; -------------------------------------------------------------------------------- /src/lib/middleware.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Middleware between web api driver and whatsappJS. 3 | * Right now this file should not import nor export anything. 4 | * Until I figure out how to compile this file with the imports inlined. 5 | * Specifically the [ExposedFn] enum. 6 | * Maybe by creating a webpack/rollup task 7 | */ 8 | var ExposedFn; 9 | (function (ExposedFn) { 10 | ExposedFn["OnMessage"] = "onMessage"; 11 | })(ExposedFn || (ExposedFn = {})); 12 | /** 13 | * Exposes [OnMessage] function 14 | */ 15 | WAPI.waitNewMessages(false, function (data) { 16 | data.forEach(function (message) { 17 | window[ExposedFn.OnMessage](message); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/lib/middleware/middleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Middleware between web api driver and whatsappJS. 3 | * Right now this file should not import nor export anything. 4 | * Until I figure out how to compile this file with the imports inlined. 5 | * Specifically the [ExposedFn] enum. 6 | * Maybe by creating a webpack/rollup task 7 | */ 8 | 9 | declare module WAPI { 10 | const waitNewMessages: (rmCallback: boolean, callback: Function) => void; 11 | const waitNewAcknowledgements: (callback: Function) => void; 12 | const onStateChange: (callback: Function) => void; 13 | const allNewMessagesListener: (callback: Function) => void; 14 | } 15 | 16 | enum ExposedFn { 17 | OnMessage = 'onMessage', 18 | OnAck = 'onAck', 19 | OnParticipantsChanged = 'onParticipantsChanged', 20 | } 21 | 22 | /** 23 | * Exposes [OnMessage] function 24 | */ 25 | WAPI.waitNewMessages(false, (data) => { 26 | data.forEach((message) => { 27 | window[ExposedFn.OnMessage](message); 28 | }); 29 | }); 30 | 31 | WAPI.waitNewAcknowledgements(function (data) { 32 | if (!Array.isArray(data)) { 33 | data = [data]; 34 | } 35 | data.forEach(function (message) { 36 | if (window[ExposedFn.OnAck]) window[ExposedFn.OnAck](message); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/lib/middleware/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "module": "es6", 5 | "target": "es6", 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/middleware/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './middleware.ts', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.tsx?$/, 9 | use: 'ts-loader', 10 | exclude: /node_modules/, 11 | }, 12 | ], 13 | }, 14 | resolve: { 15 | extensions: [ '.tsx', '.ts', '.js' ], 16 | }, 17 | output: { 18 | filename: 'middleware.js', 19 | path: path.resolve(__dirname, '../../../dist/lib/middleware'), 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/lib/wapi/business/get-business-profiles-products.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Find any product listings of the given number. Use this to query a catalog 3 | * 4 | * @param id id of buseinss profile (i.e the number with @c.us) 5 | * @param done Optional callback function for async execution 6 | * @returns None 7 | */ 8 | window.WAPI.getBusinessProfilesProducts = function (id, done) { 9 | return Store.Catalog.find(id) 10 | .then((resp) => { 11 | if (resp.msgProductCollection && resp.msgProductCollection._models.length) 12 | done(); 13 | return resp.productCollection._models; 14 | }) 15 | .catch((error) => { 16 | done(); 17 | return error.model._products; 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/wapi/business/send-message-with-buttons.js: -------------------------------------------------------------------------------- 1 | window.WAPI.sendButtons = async function (chatId) { 2 | var chat = Store.Chat.get(chatId); 3 | var tempMsg = Object.create(chat.msgs.filter((msg) => msg.__x_isSentByMe)[0]); 4 | // var tempMsg = Object.create(Store.Msg.models.filter(msg => msg.to._serialized===chatId&&msg.__x_isSentByMe&& msg.type=='chat' && !msg.quotedStanzaID)[0]) 5 | var t2 = Object.create( 6 | Store.Msg.filter((x) => (x.type == 'template') & !x.id.fromMe)[0] 7 | ); 8 | var newId = window.WAPI.getNewMessageId(chatId); 9 | delete tempMsg.hasTemplateButtons; 10 | var extend = { 11 | ack: 0, 12 | id: newId, 13 | local: !0, 14 | self: 'out', 15 | t: parseInt(new Date().getTime() / 1000), 16 | to: chat.id, 17 | isNewMsg: false, 18 | // isNewMsg: !0, 19 | type: 'template', 20 | subtype: 'text', 21 | body: 'body text', 22 | isForwarded: false, 23 | broadcast: false, 24 | isQuotedMsgAvailable: false, 25 | shouldEnableHsm: true, 26 | __x_hasTemplateButtons: true, 27 | invis: true, 28 | }; 29 | 30 | Object.assign(tempMsg, extend); 31 | 32 | var btns = new Store.Builders.HydratedFourRowTemplate({ 33 | hydratedButtons: [ 34 | new Store.Builders.HydratedTemplateButton({ 35 | quickReplyButton: new Store.Builders.HydratedQuickReplyButton({ 36 | displayText: 'test', 37 | id: '{"eventName":"inform"}', 38 | quickReplyButton: true, 39 | }), 40 | index: 0, 41 | }), 42 | new Store.Builders.HydratedTemplateButton({ 43 | callButton: new Store.Builders.HydratedCallButton({ 44 | displayText: 'test call', 45 | phoneNumber: '4477777777777', 46 | }), 47 | index: 1, 48 | }), 49 | new Store.Builders.HydratedTemplateButton({ 50 | urlButton: new Store.Builders.HydratedURLButton({ 51 | displayText: 'test url', 52 | url: 'https://google.com', 53 | }), 54 | index: 2, 55 | }), 56 | ], 57 | hydratedContentText: 'hellllloooowww', 58 | hydratedFooterText: 'asdasd', 59 | hydratedTitleText: 'asdasd232', 60 | }); 61 | 62 | Store.Parser.parseTemplateMessage(t2, btns); 63 | tempMsg.buttons = t2.buttons; 64 | console.log('t2', t2.body); 65 | tempMsg.mediaData = undefined; 66 | tempMsg.mediaObject = undefined; 67 | tempMsg._minEphemeralExpirationTimestamp(); 68 | tempMsg.senderObj.isBusiness = true; 69 | tempMsg.senderObj.isEnterprise = true; 70 | tempMsg.senderObj = { 71 | ...tempMsg.senderObj, 72 | isBusiness: true, 73 | isEnterprise: true, 74 | notifyName: 'button test', 75 | mentionName: 'Button Test', 76 | displayName: 'Button Test', 77 | searchName: 'button test', 78 | header: 'b', 79 | formattedShortNameWithNonBreakingSpaces: 'Button test', 80 | formattedShortName: 'Button test', 81 | formattedName: 'Button test', 82 | formattedUser: 'Button test', 83 | }; 84 | tempMsg.body = t2.body; 85 | tempMsg.to = tempMsg.from; 86 | tempMsg.caption = tempMsg.body; 87 | console.log('tempMsg', tempMsg); 88 | return chat.sendQueue 89 | .enqueue( 90 | chat.addQueue 91 | .enqueue( 92 | Store.MessageUtils.appendMessage(chat, tempMsg).then(() => { 93 | var e = Store.Msg.add(tempMsg)[0]; 94 | console.log('e ', e); 95 | if (e) { 96 | return e.waitForPrep().then(() => { 97 | return e; 98 | }); 99 | } 100 | }) 101 | ) 102 | .then((t) => chat.msgs.add(t)) 103 | .catch((e) => console.log(e)) 104 | ) 105 | .then((t) => { 106 | var e = t[0]; 107 | const s = Store.Base2; 108 | if (!s.BinaryProtocol) 109 | window.Store.Base2.BinaryProtocol = new window.Store.bp(11); 110 | var idUser = new Store.WidFactory.createWid(chatId); 111 | var k = Store.createMessageKey({ 112 | ...e, 113 | to: idUser, 114 | id: e.__x_id, 115 | }); 116 | console.log('key', k); 117 | var wm = new Store.WebMessageInfo({ 118 | message: new Store.Builders.Message({ 119 | // conversation:'okhellowhi', 120 | templateMessage: new Store.Builders.TemplateMessage({ 121 | hydratedFourRowTemplate: btns, 122 | hydratedTemplate: btns, 123 | }), 124 | }), 125 | key: k, 126 | messageTimestamp: e.t, 127 | multicast: undefined, 128 | url: undefined, 129 | urlNumber: undefined, 130 | clearMedia: undefined, 131 | ephemeralDuration: undefined, 132 | }); 133 | console.log('wm', wm); 134 | var action = s.actionNode('relay', [ 135 | ['message', null, Store.WebMessageInfo.encode(wm).readBuffer()], 136 | ]); 137 | console.log('action', action); 138 | var a = e.id.id; 139 | return new Promise(function (resolve, reject) { 140 | console.log('yo'); 141 | return s.binSend( 142 | 'send', 143 | action, 144 | reject, 145 | { 146 | tag: a, 147 | onSend: s.wrap((_) => { 148 | console.log('onsend', _); 149 | resolve(_); 150 | }), 151 | onDrop: s.wrap((_) => { 152 | console.log('ondrop', _); 153 | reject(_); 154 | }), 155 | retryOn5xx: !0, 156 | resendGuard: function (_) { 157 | var t = Store.Msg.get(e.id); 158 | console.log('in resend', _); 159 | return 'protocol' === e.type || (t && t.id.equals(e.id)); 160 | }, 161 | }, 162 | { 163 | debugString: ['action', 'message', e.type, e.subtype, a].join(), 164 | debugObj: { 165 | xml: action, 166 | pb: wm, 167 | }, 168 | metricName: 'MESSAGE', 169 | ackRequest: !1, 170 | } 171 | ); 172 | }); 173 | }); 174 | }; 175 | 176 | window.WAPI.sendButtons2 = async function (chatId) { 177 | var chat = Store.Chat.get(chatId); 178 | var tempMsg = Object.create( 179 | Store.Msg.models.filter( 180 | (msg) => 181 | msg.to._serialized === chatId && 182 | msg.__x_isSentByMe && 183 | msg.type == 'chat' && 184 | !msg.quotedStanzaID 185 | )[0] 186 | ); 187 | var t2 = Object.create( 188 | Store.Msg.models.filter( 189 | (msg) => 190 | msg.to._serialized === chatId && 191 | msg.__x_isSentByMe && 192 | msg.type == 'chat' && 193 | !msg.quotedStanzaID 194 | )[0] 195 | ); 196 | var newId = window.WAPI.getNewMessageId(chatId); 197 | delete tempMsg.hasTemplateButtons; 198 | var extend = { 199 | ack: 0, 200 | id: newId, 201 | local: !0, 202 | self: 'out', 203 | t: parseInt(new Date().getTime() / 1000), 204 | to: Store.WidFactory.createWid(chatId), 205 | isNewMsg: !0, 206 | type: 'template', 207 | subtype: 'text', 208 | broadcast: false, 209 | isQuotedMsgAvailable: false, 210 | shouldEnableHsm: true, 211 | __x_hasTemplateButtons: true, 212 | invis: false, 213 | }; 214 | 215 | Object.assign(tempMsg, extend); 216 | 217 | var btns = new Store.Builders.HydratedFourRowTemplate({ 218 | hydratedButtons: [ 219 | new Store.Builders.HydratedTemplateButton({ 220 | quickReplyButton: new Store.Builders.HydratedQuickReplyButton({ 221 | displayText: 'test', 222 | id: '{"eventName":"inform"}', 223 | quickReplyButton: true, 224 | }), 225 | index: 0, 226 | }), 227 | new Store.Builders.HydratedTemplateButton({ 228 | callButton: new Store.Builders.HydratedCallButton({ 229 | displayText: 'test call', 230 | phoneNumber: '4477777777777', 231 | }), 232 | index: 1, 233 | }), 234 | new Store.Builders.HydratedTemplateButton({ 235 | callButton: new Store.Builders.HydratedCallButton({ 236 | displayText: 'test call', 237 | phoneNumber: '4477777777777', 238 | }), 239 | index: 2, 240 | }), 241 | new Store.Builders.HydratedTemplateButton({ 242 | urlButton: new Store.Builders.HydratedURLButton({ 243 | displayText: 'test url', 244 | url: 'https://google.com', 245 | }), 246 | index: 3, 247 | }), 248 | ], 249 | hydratedContentText: 'hellllloooowww', 250 | hydratedFooterText: 'asdasd', 251 | hydratedTitleText: 'asdasd232', 252 | }); 253 | 254 | Store.Parser.parseTemplateMessage(t2, btns); 255 | tempMsg.buttons = t2.buttons; 256 | console.log('t2', t2.body); 257 | console.log('tempMsg', tempMsg); 258 | 259 | return chat.sendQueue 260 | .enqueue( 261 | chat.addQueue 262 | .enqueue( 263 | Store.MessageUtils.appendMessage(chat, tempMsg).then(() => { 264 | var e = Store.Msg.add(tempMsg)[0]; 265 | console.log('e ', e); 266 | if (e) { 267 | return e.waitForPrep().then(() => { 268 | return e; 269 | }); 270 | } 271 | }) 272 | ) 273 | .then((t) => chat.msgs.add(t)) 274 | .catch((e) => console.log(e)) 275 | ) 276 | .then((t) => { 277 | var e = t[0]; 278 | console.log('e', e); 279 | const s = Store.Base2; 280 | if (!s.BinaryProtocol) 281 | window.Store.Base2.BinaryProtocol = new window.Store.bp(11); 282 | var idUser = new Store.WidFactory.createWid(chatId); 283 | var k = Store.createMessageKey({ 284 | ...e, 285 | to: idUser, 286 | id: e.__x_id, 287 | }); 288 | console.log('key', k); 289 | var wm = new Store.WebMessageInfo({ 290 | message: new Store.Builders.Message({ 291 | //if you uncomment the next line then the message gets sent properly as a text 292 | // conversation:'okhellowhi', 293 | templateMessage: new Store.Builders.TemplateMessage({ 294 | hydratedFourRowTemplate: btns, 295 | hydratedTemplate: btns, 296 | }), 297 | }), 298 | key: k, 299 | messageTimestamp: e.t, 300 | }); 301 | console.log('wm', wm); 302 | var action = s.actionNode('relay', [ 303 | ['message', null, Store.WebMessageInfo.encode(wm).readBuffer()], 304 | ]); 305 | console.log('action', action); 306 | var a = e.id.id; 307 | console.log('a', a); 308 | return new Promise(function (resolve, reject) { 309 | console.log('yo'); 310 | return s.binSend( 311 | 'send', 312 | action, 313 | reject, 314 | { 315 | tag: a, 316 | onSend: s.wrap(resolve), 317 | onDrop: s.wrap(reject), 318 | retryOn5xx: !0, 319 | resendGuard: function (_) { 320 | var t = Store.Msg.get(e.id); 321 | return 'protocol' === e.type || (t && t.id.equals(e.id)); 322 | }, 323 | }, 324 | { 325 | debugString: ['action', 'message', 'chat', 'null', a].join(), 326 | debugObj: { 327 | xml: action, 328 | pb: wm, 329 | }, 330 | metricName: 'MESSAGE', 331 | ackRequest: !1, 332 | } 333 | ); 334 | }); 335 | }); 336 | }; 337 | -------------------------------------------------------------------------------- /src/lib/wapi/business/send-payment-request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Send Payment Request 3 | * 4 | * @param {string} chatId '000000000000@c.us' 5 | * @param {string} amount1000 The amount in base value / 10 (e.g 50000 in GBP = £50) 6 | * @param {string} currency Three letter currency code (e.g SAR, GBP, USD, INR, AED, EUR) 7 | * @param {string} note message to send with the payment request 8 | */ 9 | window.WAPI.sendPaymentRequest = async function ( 10 | chatId, 11 | amount1000, 12 | currency, 13 | noteMessage 14 | ) { 15 | var chat = Store.Chat.get(chatId); 16 | var tempMsg = Object.create(chat.msgs.filter((msg) => msg.__x_isSentByMe)[0]); 17 | var newId = window.WAPI.getNewMessageId(chatId); 18 | var extend = { 19 | ack: 0, 20 | id: newId, 21 | local: !0, 22 | self: 'out', 23 | t: parseInt(new Date().getTime() / 1000), 24 | to: chatId, 25 | isNewMsg: !0, 26 | type: 'payment', 27 | subtype: 'request', 28 | amount1000, 29 | requestFrom: chatId, 30 | currency, 31 | noteMessage, 32 | expiryTimestamp: parseInt( 33 | new Date(new Date().setDate(new Date().getDate() + 1)).getTime() / 1000 34 | ), 35 | }; 36 | Object.assign(tempMsg, extend); 37 | await Store.addAndSendMsgToChat(chat, tempMsg); 38 | }; 39 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/add-participant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds participant to group 3 | * @param {string} groupId Chat id ('0000000000-00000000@g.us') 4 | * @param {string} participantId Participant id'000000000000@c.us' 5 | * @param {Function} done Optional callback 6 | */ 7 | export async function addParticipant(groupId, participantId, done) { 8 | const chat = Store.Chat.get(groupId); 9 | const participant = Store.Contact.get(participantId); 10 | return window.Store.Participants.addParticipants(chat, [participant]).then( 11 | () => { 12 | done(true); 13 | return true; 14 | } 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/are-all-messages-loaded.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if all messages are loaded 3 | * @param {string} id Chat id 4 | * @param {Function} done Optional callback 5 | */ 6 | export function areAllMessagesLoaded(id, done) { 7 | const found = WAPI.getChat(id); 8 | if (!found.msgs.msgLoadState.noEarlierMsgs) { 9 | if (done) done(false); 10 | return false; 11 | } 12 | if (done) done(true); 13 | return true; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/block-contact.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Blocks contact 3 | * @param {string} id Contact id (00000000@c.us) 4 | * @param {Function} done Optional callback 5 | */ 6 | export function blockContact(id, done) { 7 | const contact = window.Store.Contact.get(id); 8 | if (contact !== undefined) { 9 | contact.setBlock(!0); 10 | done(true); 11 | return true; 12 | } 13 | done(false); 14 | return false; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/clear-chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clears chat messages 3 | * @param {string} id Chat id 4 | */ 5 | export async function clearChat(id) { 6 | return await Store.ChatUtil.sendClear(Store.Chat.get(id), true); 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/create-group.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates group 3 | * @param {string} name Group name 4 | * @param {string[]} contactsId Contacts ids 5 | */ 6 | export async function createGroup(name, contactsId) { 7 | if (!Array.isArray(contactsId)) { 8 | contactsId = [contactsId]; 9 | } 10 | 11 | return await window.Store.WapQuery.createGroup(name, contactsId); 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/delete-conversation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} chatId Chat id 3 | * @param {Function} done Optional callback 4 | */ 5 | export function deleteConversation(chatId, done) { 6 | let userId = new window.Store.UserConstructor(chatId, { 7 | intentionallyUsePrivateConstructor: true, 8 | }); 9 | let conversation = WAPI.getChat(userId); 10 | 11 | if (!conversation) { 12 | if (done !== undefined) { 13 | done(false); 14 | } 15 | return false; 16 | } 17 | 18 | window.Store.sendDelete(conversation, false) 19 | .then(() => { 20 | if (done !== undefined) { 21 | done(true); 22 | } 23 | }) 24 | .catch(() => { 25 | if (done !== undefined) { 26 | done(false); 27 | } 28 | }); 29 | 30 | return true; 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/delete-messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Deletes message of given message id 3 | * @param {string} chatId The chat id from which to delete the message. 4 | * @param {string[]} messageArray The specific message id of the message to be deleted 5 | * @param {boolean} onlyLocal If it should only delete locally (message remains on the other recipienct's phone). Defaults to false. 6 | * @param {Function} done Optional callback 7 | */ 8 | export function deleteMessages(chatId, messageArray, onlyLocal, done) { 9 | const userId = new Store.WidFactory.createWid(chatId); 10 | const conversation = WAPI.getChat(userId); 11 | if (!conversation) { 12 | if (done !== undefined) { 13 | done(false); 14 | } 15 | return false; 16 | } 17 | 18 | if (!Array.isArray(messageArray)) { 19 | messageArray = [messageArray]; 20 | } 21 | 22 | let messagesToDelete = messageArray 23 | .map((msgId) => 24 | typeof msgId == 'string' ? window.Store.Msg.get(msgId) : msgId 25 | ) 26 | .filter((x) => x); 27 | if (messagesToDelete.length == 0) return true; 28 | let jobs = onlyLocal 29 | ? [conversation.sendDeleteMsgs(messagesToDelete, conversation)] 30 | : [ 31 | conversation.sendRevokeMsgs( 32 | messagesToDelete.filter((msg) => msg.isSentByMe), 33 | conversation 34 | ), 35 | conversation.sendDeleteMsgs( 36 | messagesToDelete.filter((msg) => !msg.isSentByMe), 37 | conversation 38 | ), 39 | ]; 40 | return Promise.all(jobs).then((_) => { 41 | if (done !== undefined) { 42 | done(true); 43 | } 44 | return true; 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/demote-participant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Demotes admin privileges of participant 3 | * @param {string} groupId Chat id ('0000000000-00000000@g.us') 4 | * @param {string} participantId Participant id'000000000000@c.us' 5 | * @param {Function} done Optional callback 6 | */ 7 | export async function demoteParticipant(groupId, participantId, done) { 8 | return window.Store.WapQuery.demoteParticipants(groupId, [ 9 | participantId, 10 | ]).then(() => { 11 | const chat = Store.Chat.get(groupId); 12 | const participant = chat.groupMetadata.participants.get(participantId); 13 | window.Store.Participants.demoteParticipants(chat, [participant]).then( 14 | () => { 15 | done(true); 16 | return true; 17 | } 18 | ); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/download-file-with-credentials.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Downloads given url file 3 | * @param {string} url 4 | * @param {Function} done Optional callback 5 | */ 6 | export function downloadFileWithCredentials(url, done) { 7 | let xhr = new XMLHttpRequest(); 8 | 9 | xhr.onload = function () { 10 | if (xhr.readyState == 4) { 11 | if (xhr.status == 200) { 12 | let reader = new FileReader(); 13 | reader.readAsDataURL(xhr.response); 14 | reader.onload = function (e) { 15 | done(reader.result.substr(reader.result.indexOf(',') + 1)); 16 | }; 17 | } else { 18 | console.error(xhr.statusText); 19 | } 20 | } else { 21 | console.log(err); 22 | done(false); 23 | } 24 | }; 25 | 26 | xhr.open('GET', url, true); 27 | xhr.withCredentials = true; 28 | xhr.responseType = 'blob'; 29 | xhr.send(null); 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/encrypt-and-upload-file.js: -------------------------------------------------------------------------------- 1 | import { generateMediaKey, getFileHash } from '../helper'; 2 | 3 | /** 4 | * Encrypts given blob 5 | * @param {'audio' | 'sticker' | 'video' | 'product' | 'document' | 'gif' | 'image' | 'ptt' | 'template' | 'history' | 'ppic'} type The type of file 6 | * @param {Blob} blob 7 | */ 8 | export async function encryptAndUploadFile(type, blob) { 9 | const filehash = await getFileHash(blob); 10 | const mediaKey = generateMediaKey(32); 11 | const controller = new AbortController(); 12 | const signal = controller.signal; 13 | const encrypted = await window.Store.UploadUtils.encryptAndUpload({ 14 | blob, 15 | type, 16 | signal, 17 | mediaKey, 18 | }); 19 | return { 20 | ...encrypted, 21 | clientUrl: encrypted.url, 22 | filehash, 23 | id: filehash, 24 | uploadhash: encrypted.encFilehash, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/forward-messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Forwards array of messages (could be ids or message objects) 3 | * @param {string} to Chat id 4 | * @param {string[] | Message[]} messages Array of messages to be forwarded 5 | * @param {boolean} skipMyMessages 6 | */ 7 | export async function forwardMessages(to, messages, skipMyMessages) { 8 | if (!Array.isArray(messages)) { 9 | messages = [messages]; 10 | } 11 | const toForward = messages 12 | .map((msg) => { 13 | if (typeof msg === 'string') { 14 | return window.Store.Msg.get(msg); 15 | } else { 16 | return window.Store.Msg.get(msg.id); 17 | } 18 | }) 19 | .filter((msg) => (skipMyMessages ? !msg.__x_isSentByMe : true)); 20 | 21 | // const userId = new window.Store.UserConstructor(to); 22 | const conversation = window.Store.Chat.get(to); 23 | return conversation.forwardMessages(toForward); 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-all-chats-ids.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetches all chat IDs from store 3 | * @param done Callback (optional) 4 | * @returns {string[]} List of chat id's 5 | */ 6 | export const getAllChatIds = function (done) { 7 | const chatIds = window.Store.Chat.map( 8 | (chat) => chat.id._serialized || chat.id 9 | ); 10 | 11 | if (done !== undefined) done(chatIds); 12 | return chatIds; 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-all-chats-with-messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves chats with messages 3 | * @param {boolean} newOnly boolean 4 | * @param {Function} done callback 5 | */ 6 | export async function getAllChatsWithMessages(newOnly, done) { 7 | const x = []; 8 | if (newOnly) { 9 | x.push( 10 | WAPI.getAllChatsWithNewMsg().map((c) => WAPI.getChat(c.id._serialized)) 11 | ); 12 | } else { 13 | x.push(WAPI.getAllChatIds().map((c) => WAPI.getChat(c))); 14 | } 15 | const _result = (await Promise.all(x)).flatMap((x) => x); 16 | const result = JSON.stringify(_result); 17 | return JSON.parse(result); 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-all-chats.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetches all chat objects from store 3 | * @param done Callback (optional) 4 | * @returns {Chat[]} Array of chat objects 5 | */ 6 | export const getAllChats = function (done) { 7 | const chats = window.Store.Chat.map((chat) => WAPI._serializeChatObj(chat)); 8 | 9 | if (done !== undefined) done(chats); 10 | return chats; 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-all-contacts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves all contacts 3 | * @param {Function} done Callback function (optional) 4 | * * @returns {Array} List of contacts 5 | */ 6 | export const getAllContacts = function (done) { 7 | const contacts = window.Store.Contact.map((contact) => 8 | WAPI._serializeContactObj(contact) 9 | ); 10 | 11 | if (done !== undefined) done(contacts); 12 | return contacts; 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-all-group-metadata.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetches all group metadata objects from store 3 | * @param {Function} done Optional callback 4 | */ 5 | export function getAllGroupMetadata(done) { 6 | const groupData = window.Store.GroupMetadata.map( 7 | (groupData) => groupData.all 8 | ); 9 | 10 | if (done !== undefined) done(groupData); 11 | return groupData; 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-all-groups.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves all groups 3 | * @param {Function} done callback 4 | * @returns {Array} List of chats 5 | */ 6 | export function getAllGroups(done) { 7 | const groups = window.Store.Chat.filter((chat) => chat.isGroup); 8 | 9 | if (done !== undefined) done(groups); 10 | return groups; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-all-messages-in-chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} id Chat id 3 | * @param {boolean} includeMe 4 | * @param {boolean} includeNotifications 5 | * @param {Function} done Optional callback 6 | */ 7 | export function getAllMessagesInChat( 8 | id, 9 | includeMe, 10 | includeNotifications, 11 | done 12 | ) { 13 | const chat = WAPI.getChat(id); 14 | let output = []; 15 | const messages = chat.msgs._models; 16 | 17 | for (const i in messages) { 18 | if (i === 'remove') { 19 | continue; 20 | } 21 | const messageObj = messages[i]; 22 | 23 | let message = WAPI.processMessageObj( 24 | messageObj, 25 | includeMe, 26 | includeNotifications 27 | ); 28 | if (message) output.push(message); 29 | } 30 | if (done !== undefined) done(output); 31 | return output; 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-all-new-messages.js: -------------------------------------------------------------------------------- 1 | import { getAllChatsWithNewMessages } from './get-chats-with-new-messages'; 2 | 3 | /** 4 | * Retrieves all new messages 5 | * TODO: Test, seems to be written incorrectly 6 | */ 7 | export const getAllNewMessages = function () { 8 | const _newMessages = JSON.stringify( 9 | getAllChatsWithNewMessages() 10 | .map((c) => WAPI.getChat(c.id._serialized)) 11 | .map((c) => c.msgs._models.filter((x) => x.isNewMsg)) || [] 12 | ); 13 | 14 | return JSON.parse(_newMessages); 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-all-unread-messages.js: -------------------------------------------------------------------------------- 1 | import { getAllChatsWithNewMessages } from './get-chats-with-new-messages'; 2 | 3 | /** 4 | * Retrieves undread messages 5 | * x.ack === -1 6 | * TODO: Test this fn, seems incorrect, should not be async 7 | */ 8 | export const getAllUnreadMessages = async function () { 9 | const _partials = JSON.stringify( 10 | getAllChatsWithNewMessages() 11 | .map((c) => WAPI.getChat(c.id._serialized)) 12 | .map((c) => c.msgs._models.filter((x) => x.ack === -1)) 13 | .flatMap((x) => x) || [] 14 | ); 15 | 16 | const partials = JSON.parse(_partials); 17 | return partials; 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-battery-level.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrives battery level 3 | * If plugged in, it will return 100% 4 | * @returns {number} 5 | */ 6 | export function getBatteryLevel() { 7 | if (window.Store.Conn.plugged) { 8 | return 100; 9 | } 10 | output = window.Store.Conn.battery; 11 | if (done !== undefined) { 12 | done(output); 13 | } 14 | return output; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-chat-by-id.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves a chat by given id 3 | * @param {string} id 4 | * @param {Function} done optional callback 5 | */ 6 | export function getChatById(id, done) { 7 | let found = WAPI.getChat(id); 8 | if (found) { 9 | found = WAPI._serializeChatObj(found); 10 | } else { 11 | found = false; 12 | } 13 | 14 | if (done !== undefined) done(found); 15 | return found; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-chat-by-name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves chat by its name 3 | * @param {string} name Chat name 4 | * @param {Function} done callback 5 | */ 6 | export function getChatByName(name, done) { 7 | const found = window.Store.Chat.find((chat) => chat.name === name); 8 | if (done !== undefined) done(found); 9 | return found; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves chat by its id 3 | * @param {*} id Id of the chat 4 | * @param {*} done Callback 5 | * @returns {Chat} object 6 | */ 7 | export function getChat(id, done) { 8 | id = typeof id == 'string' ? id : id._serialized; 9 | const found = window.Store.Chat.get(id); 10 | if (found) 11 | found.sendMessage = found.sendMessage 12 | ? found.sendMessage 13 | : function () { 14 | return window.Store.sendMessage.apply(this, arguments); 15 | }; 16 | if (done !== undefined) done(found); 17 | return found; 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-chats-with-new-messages.js: -------------------------------------------------------------------------------- 1 | import { hasUndreadMessages } from './has-unread-messages'; 2 | 3 | /** 4 | * Retrieves chats with undread/new messages 5 | * @param {*} done 6 | * @returns {Chat[]} chat list 7 | */ 8 | export const getAllChatsWithNewMessages = function (done) { 9 | const chats = window.Store.Chat.filter(hasUndreadMessages).map((chat) => 10 | WAPI._serializeChatObj(chat) 11 | ); 12 | 13 | if (done !== undefined) done(chats); 14 | return chats; 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-common-groups.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves common groups of given participant 3 | * @param {string} participantId 4 | * @param {Function} done Optional callback 5 | */ 6 | export async function getCommonGroups(participantId, done) { 7 | let output = []; 8 | groups = window.WAPI.getAllGroups(); 9 | for (let idx in groups) { 10 | try { 11 | participants = await window.WAPI.getGroupParticipantIDs(groups[idx].id); 12 | if ( 13 | participants.filter((participant) => participant == participantId) 14 | .length 15 | ) { 16 | output.push(groups[idx]); 17 | } 18 | } catch (err) { 19 | console.log('Error in group:'); 20 | console.log(groups[idx]); 21 | console.log(err); 22 | } 23 | } 24 | 25 | if (done !== undefined) { 26 | done(output); 27 | } 28 | return output; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-contact.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetches contact object from store by ID 3 | * @param {string} id constact id 4 | * @param {Function} done Callback (optional) 5 | * @returns {Contact} contact object 6 | */ 7 | export const getContact = function (id, done) { 8 | const found = window.Store.Contact.get(id); 9 | 10 | if (done !== undefined) done(window.WAPI._serializeContactObj(found)); 11 | return window.WAPI._serializeContactObj(found); 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-group-admins.js: -------------------------------------------------------------------------------- 1 | import { _getGroupParticipants } from './get-group-participants'; 2 | 3 | /** 4 | * Retrieves group admins 5 | * @param {string} id Group id 6 | * @param {Function} done Optional callback 7 | */ 8 | export async function getGroupAdmins(id, done) { 9 | const output = (await _getGroupParticipants(id)) 10 | .filter((participant) => participant.isAdmin) 11 | .map((admin) => admin.id); 12 | 13 | if (done !== undefined) done(output); 14 | return output; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-group-invite-link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates group invite link 3 | * @param {string} chatId 4 | * @returns {string} Group invite link 5 | */ 6 | export async function getGroupInviteLink(chatId) { 7 | var chat = Store.Chat.get(chatId); 8 | if (!chat.isGroup) return ''; 9 | await Store.GroupInvite.queryGroupInviteCode(chat); 10 | return `https://chat.whatsapp.com/${chat.inviteCode}`; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-group-metadata.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetches group metadata object from store by ID 3 | * @param {string} id Group id 4 | * @param {Function} done Optional callback 5 | * @returns Group metadata object 6 | */ 7 | export async function getGroupMetadata(id, done) { 8 | let output = window.Store.GroupMetadata.find(id); 9 | if (done !== undefined) done(output); 10 | return output; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-group-participant-ids.js: -------------------------------------------------------------------------------- 1 | import { _getGroupParticipants } from './get-group-participants'; 2 | 3 | /** 4 | * Fetches IDs of group participants 5 | * @param {string} groupId Group id 6 | * @param {Function} done Optional callback 7 | * @returns {Promise.} Yields list of IDs 8 | */ 9 | export async function getGroupParticipantIDs(groupId, done) { 10 | const output = (await _getGroupParticipants(groupId)).map( 11 | (participant) => participant.id 12 | ); 13 | 14 | if (done !== undefined) done(output); 15 | return output; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-group-participants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetches group participants 3 | * @param {string} id Group id 4 | */ 5 | export async function _getGroupParticipants(id) { 6 | const metadata = await WAPI.getGroupMetadata(id); 7 | return metadata.participants; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-host.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns an object with all of your host device details 3 | */ 4 | export function getHost() { 5 | return Store.Me.attributes; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-me.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves current user object 3 | */ 4 | export function getMe(done) { 5 | const rawMe = window.Store.Contact.get(window.Store.Conn.me); 6 | 7 | if (done !== undefined) done(rawMe.all); 8 | return rawMe.all; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-message-by-id.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves message by given id 3 | * @param {string} id Message id 4 | * @param {Function} done 5 | */ 6 | export function getMessageById(id, done) { 7 | let result = false; 8 | try { 9 | let msg = window.Store.Msg.get(id); 10 | if (msg) { 11 | result = WAPI.processMessageObj(msg, true, true); 12 | } 13 | } catch (err) {} 14 | 15 | if (done !== undefined) { 16 | done(result); 17 | } else { 18 | return result; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-my-contacts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves contacts filtered with `isMyContact` property 3 | * @param {Function} done Callback function (optional) 4 | * @returns {Array} List of contacts 5 | */ 6 | export const getMyContacts = function (done) { 7 | const contacts = window.Store.Contact.filter( 8 | (contact) => contact.isMyContact === true 9 | ).map((contact) => WAPI._serializeContactObj(contact)); 10 | if (done !== undefined) done(contacts); 11 | return contacts; 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-new-id.js: -------------------------------------------------------------------------------- 1 | export function getNewId() { 2 | var text = ''; 3 | var possible = 4 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 5 | 6 | for (var i = 0; i < 20; i++) 7 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 8 | return text; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-new-message-id.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates new message id based on given chat 3 | * @param {*} chatId Chat id 4 | */ 5 | export function getNewMessageId(chatId) { 6 | const newMsgId = new Store.MsgKey( 7 | Object.assign({}, Store.Msg.models[0].__x_id) 8 | ); 9 | 10 | newMsgId.fromMe = true; 11 | newMsgId.id = WAPI.getNewId().toUpperCase(); 12 | newMsgId.remote = chatId; 13 | newMsgId._serialized = `${newMsgId.fromMe}_${newMsgId.remote}_${newMsgId.id}`; 14 | return newMsgId; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-number-profile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if a number is a valid whatsapp number 3 | * @param {*} id User id (you need to include the @c.us at the end.) 4 | * @param {*} done 5 | * @returns contact detial 6 | */ 7 | export async function getNumberProfile(id, done) { 8 | try { 9 | const result = await window.Store.WapQuery.queryExist(id); 10 | if (result.jid === undefined) throw 404; 11 | const data = window.WAPI._serializeNumberStatusObj(result); 12 | if (data.status == 200) data.numberExists = true; 13 | if (done !== undefined) { 14 | done(window.WAPI._serializeNumberStatusObj(result)); 15 | done(data); 16 | } 17 | return data; 18 | } catch (e) { 19 | if (done !== undefined) { 20 | done( 21 | window.WAPI._serializeNumberStatusObj({ 22 | status: e, 23 | jid: id, 24 | }) 25 | ); 26 | } 27 | return e; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-profile-pic-from-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @returns Url of the chat picture. 3 | * @param {string} id Chat id 4 | */ 5 | export function getProfilePicFromServer(id) { 6 | return Store.WapQuery.profilePicFind(id).then((x) => x.eurl); 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-status.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves satus 3 | * @param {string} to '000000000000@c.us' 4 | * 5 | * TODO: Test this function 6 | */ 7 | export async function getStatus(id) { 8 | return await Store.MyStatus.getStatus(id); 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-unread-messages-in-chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves unread messages from chat and mark them as read as a regular UX 3 | * @param {string} id Chat id 4 | * @param {boolean} includeMe Include user client messages 5 | * @param {boolean} includeNotifications Include notifications 6 | * @param {Function} done 7 | */ 8 | export function getUnreadMessagesInChat( 9 | id, 10 | includeMe, 11 | includeNotifications, 12 | done 13 | ) { 14 | // get chat and its messages 15 | let chat = WAPI.getChat(id); 16 | let messages = chat.msgs._models; 17 | 18 | // initialize result list 19 | let output = []; 20 | 21 | // look for unread messages, newest is at the end of array 22 | for (let i = messages.length - 1; i >= 0; i--) { 23 | // system message: skip it 24 | if (i === 'remove') { 25 | continue; 26 | } 27 | 28 | // get message 29 | let messageObj = messages[i]; 30 | 31 | // found a read message: stop looking for others 32 | if ( 33 | typeof messageObj.isNewMsg !== 'boolean' || 34 | messageObj.isNewMsg === false 35 | ) { 36 | continue; 37 | } else { 38 | messageObj.isNewMsg = false; 39 | // process it 40 | let message = WAPI.processMessageObj( 41 | messageObj, 42 | includeMe, 43 | includeNotifications 44 | ); 45 | 46 | // save processed message on result list 47 | if (message) output.push(message); 48 | } 49 | } 50 | // callback was passed: run it 51 | if (done !== undefined) done(output); 52 | // return result list 53 | return output; 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/get-unread-messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieves all undread Messages 3 | * @param {boolean} includeMe 4 | * @param {boolean} includeNotifications 5 | * @param {boolean} useUnreadCount 6 | * @param {Function} done Optional callback 7 | */ 8 | export function getUnreadMessages( 9 | includeMe, 10 | includeNotifications, 11 | useUnreadCount, 12 | done 13 | ) { 14 | const chats = window.Store.Chat.models; 15 | const output = []; 16 | 17 | for (const chat in chats) { 18 | if (isNaN(chat)) { 19 | continue; 20 | } 21 | 22 | const messageGroupObj = chats[chat]; 23 | let messageGroup = WAPI._serializeChatObj(messageGroupObj); 24 | messageGroup.messages = []; 25 | 26 | const messages = messageGroupObj.msgs._models; 27 | for (let i = messages.length - 1; i >= 0; i--) { 28 | const messageObj = messages[i]; 29 | if ( 30 | typeof messageObj.isNewMsg != 'boolean' || 31 | messageObj.isNewMsg === false 32 | ) { 33 | continue; 34 | } else { 35 | messageObj.isNewMsg = false; 36 | let message = WAPI.processMessageObj( 37 | messageObj, 38 | includeMe, 39 | includeNotifications 40 | ); 41 | if (message) { 42 | messageGroup.messages.push(message); 43 | } 44 | } 45 | } 46 | 47 | if (messageGroup.messages.length > 0) { 48 | output.push(messageGroup); 49 | } else { 50 | // No messages with isNewMsg true 51 | if (useUnreadCount) { 52 | // Will use unreadCount attribute to fetch last n messages from sender 53 | let n = messageGroupObj.unreadCount; 54 | for (let i = messages.length - 1; i >= 0; i--) { 55 | const messageObj = messages[i]; 56 | if (n > 0) { 57 | if (!messageObj.isSentByMe) { 58 | let message = WAPI.processMessageObj( 59 | messageObj, 60 | includeMe, 61 | includeNotifications 62 | ); 63 | messageGroup.messages.unshift(message); 64 | n -= 1; 65 | } 66 | } else if (n === -1) { 67 | // chat was marked as unread so will fetch last message as unread 68 | if (!messageObj.isSentByMe) { 69 | let message = WAPI.processMessageObj( 70 | messageObj, 71 | includeMe, 72 | includeNotifications 73 | ); 74 | messageGroup.messages.unshift(message); 75 | break; 76 | } 77 | } else { 78 | // unreadCount = 0 79 | break; 80 | } 81 | } 82 | if (messageGroup.messages.length > 0) { 83 | messageGroupObj.unreadCount = 0; // reset unread counter 84 | output.push(messageGroup); 85 | } 86 | } 87 | } 88 | } 89 | if (done !== undefined) { 90 | done(output); 91 | } 92 | return output; 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/has-unread-messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if given chat has new messages 3 | * @param {Chat} chat Chat object 4 | */ 5 | export const hasUndreadMessages = function(chat) { 6 | return chat.unreadCount > 0; 7 | }; -------------------------------------------------------------------------------- /src/lib/wapi/functions/index.js: -------------------------------------------------------------------------------- 1 | export { addParticipant } from './add-participant'; 2 | export { areAllMessagesLoaded } from './are-all-messages-loaded'; 3 | export { blockContact } from './block-contact'; 4 | export { clearChat } from './clear-chat'; 5 | export { createGroup } from './create-group'; 6 | export { deleteConversation } from './delete-conversation'; 7 | export { deleteMessages } from './delete-messages'; 8 | export { demoteParticipant } from './demote-participant'; 9 | export { downloadFileWithCredentials } from './download-file-with-credentials'; 10 | export { encryptAndUploadFile } from './encrypt-and-upload-file'; 11 | export { forwardMessages } from './forward-messages'; 12 | export { getAllChats } from './get-all-chats'; 13 | export { getAllChatIds } from './get-all-chats-ids'; 14 | export { getAllChatsWithMessages } from './get-all-chats-with-messages'; 15 | export { getAllContacts } from './get-all-contacts'; 16 | export { getAllGroupMetadata } from './get-all-group-metadata'; 17 | export { getAllGroups } from './get-all-groups'; 18 | export { getAllMessagesInChat } from './get-all-messages-in-chat'; 19 | export { getAllNewMessages } from './get-all-new-messages'; 20 | export { getAllUnreadMessages } from './get-all-unread-messages'; 21 | export { getBatteryLevel } from './get-battery-level'; 22 | export { getChat } from './get-chat'; 23 | export { getChatById } from './get-chat-by-id'; 24 | export { getChatByName } from './get-chat-by-name'; 25 | export { getAllChatsWithNewMessages } from './get-chats-with-new-messages'; 26 | export { getCommonGroups } from './get-common-groups'; 27 | export { getContact } from './get-contact'; 28 | export { getGroupAdmins } from './get-group-admins'; 29 | export { getGroupInviteLink } from './get-group-invite-link'; 30 | export { getGroupMetadata } from './get-group-metadata'; 31 | export { getGroupParticipantIDs } from './get-group-participant-ids'; 32 | export { _getGroupParticipants } from './get-group-participants'; 33 | export { getHost } from './get-host'; 34 | export { getMe } from './get-me'; 35 | export { getMessageById } from './get-message-by-id'; 36 | export { getMyContacts } from './get-my-contacts'; 37 | export { getNewId } from './get-new-id'; 38 | export { getNewMessageId } from './get-new-message-id'; 39 | export { getNumberProfile } from './get-number-profile'; 40 | export { getProfilePicFromServer } from './get-profile-pic-from-server'; 41 | export { getStatus } from './get-status'; 42 | export { getUnreadMessages } from './get-unread-messages'; 43 | export { getUnreadMessagesInChat } from './get-unread-messages-in-chat'; 44 | export { hasUndreadMessages } from './has-unread-messages'; 45 | export { isConnected } from './is-connected'; 46 | export { isLoggedIn } from './is-logged-in'; 47 | export { leaveGroup } from './leave-group'; 48 | export { loadAllEarlierMessages } from './load-all-earlier-chat-messages'; 49 | export { loadAndGetAllMessagesInChat } from './load-and-get-all-messages-in-chat'; 50 | export { loadChatEarlierMessages } from './load-earlier-chat-messages'; 51 | export { loadEarlierMessagesTillDate } from './load-earlier-messages-til-date'; 52 | export { processFiles } from './process-files'; 53 | export { processMessageObj } from './process-message-object'; 54 | export { promoteParticipant } from './promote-participant'; 55 | export { removeParticipant } from './remove-participant'; 56 | export { reply } from './reply'; 57 | export { revokeGroupInviteLink } from './revoke-invite-link'; 58 | export { sendChatstate } from './send-chat-state'; 59 | export { sendContact } from './send-contact'; 60 | export { sendFile } from './send-file'; 61 | export { sendImage } from './send-image'; 62 | export { sendImageAsSticker } from './send-image-as-stricker'; 63 | export { sendImageWithProduct } from './send-image-with-product'; 64 | export { sendLocation } from './send-location'; 65 | export { sendMessage } from './send-message'; 66 | export { sendMessageToID } from './send-message-to-id'; 67 | export { sendMessageWithTags } from './send-message-with-tags'; 68 | export { sendMessageWithThumb } from './send-message-with-thumb'; 69 | export { sendMessage2 } from './send-message2'; 70 | export { sendSeen } from './send-seen'; 71 | export { sendSticker } from './send-sticker'; 72 | export { sendVideoAsGif } from './send-video-as-gif'; 73 | export { setMyName } from './set-my-name'; 74 | export { setMyStatus } from './set-my-status'; 75 | export { startTyping, stopTyping } from './simulate-typing'; 76 | export { unblockContact } from './unblock-contact'; 77 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/is-connected.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if phone is connected 3 | * @param {Function} done Optional callback 4 | */ 5 | export function isConnected(done) { 6 | // Phone Disconnected icon appears when phone is disconnected from the tnternet 7 | const isConnected = 8 | document.querySelector('*[data-icon="alert-phone"]') !== null 9 | ? false 10 | : true; 11 | 12 | if (done !== undefined) done(isConnected); 13 | return isConnected; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/is-logged-in.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Function} done Optional callback 3 | * @returns {boolean} true if logged in, false otherwise 4 | */ 5 | export function isLoggedIn(done) { 6 | // Contact always exists when logged in 7 | const isLogged = 8 | window.Store.Contact && window.Store.Contact.checksum !== undefined; 9 | 10 | if (done !== undefined) done(isLogged); 11 | return isLogged; 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/leave-group.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Leaves group 3 | * @param {string} groupId The group id 4 | * @returns Promise 5 | */ 6 | export async function leaveGroup(groupId) { 7 | groupId = typeof groupId == 'string' ? groupId : groupId._serialized; 8 | var group = WAPI.getChat(groupId); 9 | return Store.GroupActions.sendExitGroup(group); 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/load-all-earlier-chat-messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Loads all earlier messages of given chat id 3 | * @param {string} id Chat id 4 | * @param {Funciton} done Optional callback 5 | */ 6 | export async function loadAllEarlierMessages(id, done) { 7 | const found = WAPI.getChat(id); 8 | while (!found.msgs.msgLoadState.noEarlierMsgs) { 9 | await found.loadEarlierMsgs(); 10 | } 11 | debugger; 12 | return true; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/load-and-get-all-messages-in-chat.js: -------------------------------------------------------------------------------- 1 | const promiseTimeout = function (ms, promise) { 2 | // Create a promise that rejects in milliseconds 3 | let timeout = new Promise((resolve, reject) => { 4 | let id = setTimeout(() => { 5 | clearTimeout(id); 6 | reject('Timed out in ' + ms + 'ms.'); 7 | }, ms); 8 | }); 9 | 10 | // Returns a race between our timeout and the passed in promise 11 | return Promise.race([promise, timeout]); 12 | }; 13 | 14 | /** 15 | * Loads and Retrieves all Messages in a chat 16 | * @param {string} id Chat id 17 | * @param {boolean} includeMe 18 | * @param {boolean} includeNotifications 19 | * @param {Function} done Optional callback 20 | */ 21 | export async function loadAndGetAllMessagesInChat( 22 | id, 23 | includeMe, 24 | includeNotifications, 25 | done 26 | ) { 27 | return WAPI.loadAllEarlierMessages(id).then((_) => { 28 | const chat = WAPI.getChat(id); 29 | let output = []; 30 | const messages = chat.msgs._models; 31 | 32 | for (const i in messages) { 33 | if (i === 'remove') { 34 | continue; 35 | } 36 | const messageObj = messages[i]; 37 | 38 | try { 39 | let message = WAPI.processMessageObj( 40 | messageObj, 41 | includeMe, 42 | includeNotifications 43 | ); 44 | if (message) output.push(message); 45 | } catch (err) { 46 | console.log(i); 47 | console.log(err); 48 | } 49 | } 50 | if (done !== undefined) done(output); 51 | return output; 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/load-earlier-chat-messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Loads earlier chat messages from server 3 | * @param {string} id Chat id 4 | * @param {Function} done Optional callback 5 | */ 6 | export function loadChatEarlierMessages(id, done) { 7 | const found = WAPI.getChat(id); 8 | if (done !== undefined) { 9 | found.loadEarlierMsgs().then(function () { 10 | done(); 11 | }); 12 | } else { 13 | found.loadEarlierMsgs(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/load-earlier-messages-til-date.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Loads earlier messages from Store till given date 3 | * @param {string} id Chat id 4 | * @param {*} lastMessage UTC timestamp of last message to be loaded 5 | * @param {Function} done Optional callback 6 | */ 7 | export function loadEarlierMessagesTillDate(id, lastMessage, done) { 8 | const found = WAPI.getChat(id); 9 | x = function () { 10 | if ( 11 | found.msgs.models[0].t > lastMessage && 12 | !found.msgs.msgLoadState.noEarlierMsgs 13 | ) { 14 | found.loadEarlierMsgs().then(x); 15 | } else { 16 | done(); 17 | } 18 | }; 19 | x(); 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/process-files.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Process files using Store MediaCollection 3 | * @param {Chat} chat Chat object 4 | * @param {any[]} blobs Blobs to process 5 | */ 6 | export async function processFiles(chat, blobs) { 7 | if (!Array.isArray(blobs)) { 8 | blobs = [blobs]; 9 | } 10 | 11 | const mediaCollection = new Store.MediaCollection(chat); 12 | if (Debug.VERSION !== '0.4.613') { 13 | blobs = blobs.map((blob) => ({ file: blob })); 14 | } 15 | 16 | await mediaCollection.processFiles(blobs, chat, 1); 17 | return mediaCollection; 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/process-message-object.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serializes message object 3 | * This is an original Whatsapp-Wrapper function 4 | * TODO: Check this funciton 5 | * @param {any} messageObj 6 | * @param {boolean} includeMe 7 | * @param {boolean} includeNotifications 8 | */ 9 | export function processMessageObj(messageObj, includeMe, includeNotifications) { 10 | if (messageObj.isNotification) { 11 | if (includeNotifications) return WAPI._serializeMessageObj(messageObj); 12 | else return; 13 | // System message 14 | // (i.e. "Messages you send to this chat and calls are now secured with end-to-end encryption...") 15 | } else if (messageObj.id.fromMe === false || includeMe) { 16 | return WAPI._serializeMessageObj(messageObj); 17 | } 18 | return; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/promote-participant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Promotes participant as Admin in given group 3 | * @param {string} groupId Chat id ('0000000000-00000000@g.us') 4 | * @param {string} participantId Participant id'000000000000@c.us' 5 | * @param {Function} done Optional callback 6 | */ 7 | export async function promoteParticipant(groupId, particiapntId, done) { 8 | const chat = Store.Chat.get(groupId); 9 | const participant = chat.groupMetadata.participants.get(particiapntId); 10 | return window.Store.Participants.promoteParticipants(chat, [ 11 | participant, 12 | ]).then(() => { 13 | done(true); 14 | return true; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/remove-participant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes participant from group 3 | * @param {string} groupId Chat id ('0000000000-00000000@g.us') 4 | * @param {string} participantId Participant id'000000000000@c.us' 5 | * @param {Function} done Optional callback 6 | */ 7 | export async function removeParticipant(groupId, participantId, done) { 8 | const chat = Store.Chat.get(groupId); 9 | const participant = chat.groupMetadata.participants.get(participantId); 10 | return window.Store.Participants.removeParticipants(chat, [participant]).then( 11 | () => { 12 | done(true); 13 | return true; 14 | } 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/reply.js: -------------------------------------------------------------------------------- 1 | import { getNewMessageId } from './get-new-message-id'; 2 | 3 | /** 4 | * Replys to message of given chat id 5 | * @param {string} chatId 6 | * @param {string} body 7 | * @param {string | Message} quotedMsg Message id or message object 8 | */ 9 | export async function reply(chatId, body, quotedMsg) { 10 | if (typeof quotedMsg !== 'object') { 11 | quotedMsg = Store.Msg.get(quotedMsg); 12 | } 13 | 14 | const chat = Store.Chat.get(chatId); 15 | const extras = { 16 | quotedParticipant: quotedMsg.author || quotedMsg.from, 17 | quotedStanzaID: quotedMsg.id.id, 18 | }; 19 | 20 | let tempMsg = Object.create(chat.msgs.filter((msg) => msg.__x_isSentByMe)[0]); 21 | const newId = getNewMessageId(chatId); 22 | const extend = { 23 | ack: 0, 24 | id: newId, 25 | local: !0, 26 | self: 'out', 27 | t: parseInt(new Date().getTime() / 1000), 28 | to: chatId, 29 | isNewMsg: !0, 30 | type: 'chat', 31 | quotedMsg, 32 | body, 33 | ...extras, 34 | }; 35 | Object.assign(tempMsg, extend); 36 | await Store.addAndSendMsgToChat(chat, tempMsg); 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/revoke-invite-link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Revokes group invite link 3 | * @param {string} chatId 4 | */ 5 | export async function revokeGroupInviteLink(chatId) { 6 | var chat = Store.Chat.get(chatId); 7 | if (!chat.isGroup) return false; 8 | await Store.GroupInvite.revokeGroupInvite(chat); 9 | return true; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-chat-state.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The state you want to set for the chat. Can be TYPING (1), RECRDING (2) or PAUSED (3); 3 | * @param {number} state 4 | * @param {string} chatId 5 | * @returns {boolean} true if success, false otherwise 6 | */ 7 | export async function sendChatstate(state, chatId) { 8 | switch (state) { 9 | case 0: 10 | await window.Store.ChatStates.sendChatStateComposing(chatId); 11 | break; 12 | case 1: 13 | await window.Store.ChatStates.sendChatStateRecording(chatId); 14 | break; 15 | case 2: 16 | await window.Store.ChatStates.sendChatStatePaused(chatId); 17 | break; 18 | default: 19 | return false; 20 | } 21 | return true; 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-contact.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sends contact card to iven chat id 3 | * @param {string} to Chat id 4 | * @param {string | string[]} contact Example: 0000@c.us | [000@c.us, 1111@c.us] 5 | */ 6 | export function sendContact(to, contact) { 7 | if (!Array.isArray(contact)) { 8 | contact = [contact]; 9 | } 10 | 11 | contact = contact.map((c) => { 12 | return WAPI.getChat(c).__x_contact; 13 | }); 14 | 15 | if (contact.length > 1) { 16 | window.WAPI.getChat(to).sendContactList(contact); 17 | } else if (contact.length === 1) { 18 | window.WAPI.getChat(to).sendContact(contact[0]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-file.js: -------------------------------------------------------------------------------- 1 | import { processFiles } from './process-files'; 2 | import { base64ToFile } from '../helper'; 3 | 4 | /** 5 | * Sends image to given chat if 6 | * @param {string} imgBase64 base64 encoded file 7 | * @param {string} chatid Chat id 8 | * @param {string} filename 9 | * @param {string} caption 10 | * @param {Function} done Optional callback 11 | */ 12 | export function sendFile(imgBase64, chatid, filename, caption, done) { 13 | const idUser = new Store.WidFactory.createWid(chatid); 14 | return Store.Chat.find(idUser).then((chat) => { 15 | var mediaBlob = base64ToFile(imgBase64, filename); 16 | processFiles(chat, mediaBlob).then((mediaCollection) => { 17 | var media = mediaCollection.models[0]; 18 | media.sendToChat(chat, { caption: caption }); 19 | if (done !== undefined) done(true); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-image-as-stricker.js: -------------------------------------------------------------------------------- 1 | import { base64ToFile } from '../helper/base64-to-file'; 2 | import { sendSticker } from './send-sticker'; 3 | 4 | /** 5 | * Sends image as sticker to given chat id 6 | * @param {string} imageBase64 Image as base64 7 | * @param {string} chatId chat id 8 | * @param {*} metadata sharp metadata: (https://sharp.pixelplumbing.com/api-input#metadata) 9 | */ 10 | export async function sendImageAsSticker(imageBase64, chatId, metadata) { 11 | const mediaBlob = base64ToFile( 12 | 'data:image/webp;base64,' + imageBase64, 13 | 'file.webp' 14 | ); 15 | const encrypted = await window.WAPI.encryptAndUploadFile( 16 | 'sticker', 17 | mediaBlob 18 | ); 19 | 20 | return await sendSticker(encrypted, chatId, metadata); 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-image-with-product.js: -------------------------------------------------------------------------------- 1 | import { processFiles } from './process-files'; 2 | import { base64ToFile } from '../helper'; 3 | 4 | /** 5 | * Sends product with product image to given chat id 6 | * @param {string} imgBase64 Base64 image data 7 | * @param {string} chatid Chat id 8 | * @param {string} caption Caption 9 | * @param {string} bizNumber string the @c.us number of the business account from which you want to grab the product 10 | * @param {string} productId string the id of the product within the main catalog of the aforementioned business 11 | * @param {Function} done Optional callback 12 | */ 13 | export function sendImageWithProduct( 14 | imgBase64, 15 | chatid, 16 | caption, 17 | bizNumber, 18 | productId, 19 | done 20 | ) { 21 | Store.Catalog.findCarouselCatalog(bizNumber).then((cat) => { 22 | if (cat && cat[0]) { 23 | const product = cat[0].productCollection.get(productId); 24 | const temp = { 25 | productMsgOptions: { 26 | businessOwnerJid: product.catalogWid.toString({ 27 | legacy: !0, 28 | }), 29 | productId: product.id.toString(), 30 | url: product.url, 31 | productImageCount: product.productImageCollection.length, 32 | title: product.name, 33 | description: product.description, 34 | currencyCode: product.currency, 35 | priceAmount1000: product.priceAmount1000, 36 | type: 'product', 37 | }, 38 | caption, 39 | }; 40 | 41 | var idUser = new Store.WidFactory.createWid(chatid); 42 | 43 | return Store.Chat.find(idUser).then((chat) => { 44 | var mediaBlob = base64ToFile(imgBase64, filename); 45 | // var mc = new Store.MediaCollection(chat); 46 | // mc.processFiles([mediaBlob], chat, 1) 47 | processFiles(chat, mediaBlob).then((mc) => { 48 | var media = mc.models[0]; 49 | Object.entries(temp.productMsgOptions).map( 50 | ([k, v]) => (media.mediaPrep._mediaData[k] = v) 51 | ); 52 | media.mediaPrep.sendToChat(chat, temp); 53 | if (done !== undefined) done(true); 54 | }); 55 | }); 56 | } 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-image.js: -------------------------------------------------------------------------------- 1 | import { sendFile } from './send-file'; 2 | 3 | /** 4 | * Sends image to given chat if 5 | * @param {string} imgBase64 base64 encoded file 6 | * @param {string} chatid Chat id 7 | * @param {string} filename 8 | * @param {string} caption 9 | * @param {Function} done Optional callback 10 | */ 11 | export function sendImage(imgBase64, chatid, filename, caption, done) { 12 | return sendFile(imgBase64, chatid, filename, caption, done); 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-location.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sends location to given chat id 3 | * @param {string} chatId Chat id 4 | * @param {string} latitude 5 | * @param {string} longitude 6 | * @param {string} caption 7 | */ 8 | export async function sendLocation(chatId, latitude, longitude, caption) { 9 | const chat = Store.Chat.get(chatId); 10 | let tempMsg = Object.create(chat.msgs.filter((msg) => msg.__x_isSentByMe)[0]); 11 | const newId = window.WAPI.getNewMessageId(chatId); 12 | const extend = { 13 | ack: 0, 14 | id: newId, 15 | local: !0, 16 | self: 'out', 17 | t: parseInt(new Date().getTime() / 1000), 18 | to: chatId, 19 | isNewMsg: !0, 20 | type: 'location', 21 | lat: latitude, 22 | lng: longitude, 23 | loc: caption, 24 | clientUrl: undefined, 25 | directPath: undefined, 26 | filehash: undefined, 27 | uploadhash: undefined, 28 | mediaKey: undefined, 29 | isQuotedMsgAvailable: false, 30 | invis: false, 31 | mediaKeyTimestamp: undefined, 32 | mimetype: undefined, 33 | height: undefined, 34 | width: undefined, 35 | ephemeralStartTimestamp: undefined, 36 | body: undefined, 37 | mediaData: undefined, 38 | isQuotedMsgAvailable: false, 39 | }; 40 | 41 | Object.assign(tempMsg, extend); 42 | return await Promise.all(Store.addAndSendMsgToChat(chat, tempMsg)); 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-message-to-id.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sends message to given chat id 3 | * @param {string} id 4 | * @param {string} message 5 | * @param {Function} done 6 | */ 7 | export function sendMessageToID(id, message, done) { 8 | try { 9 | window.getContact = (id) => { 10 | return Store.WapQuery.queryExist(id); 11 | }; 12 | window.getContact(id).then((contact) => { 13 | if (contact.status === 404) { 14 | done(true); 15 | } else { 16 | Store.Chat.find(contact.jid) 17 | .then((chat) => { 18 | chat.sendMessage(message); 19 | return true; 20 | }) 21 | .catch((reject) => { 22 | if (WAPI.sendMessage(id, message)) { 23 | done(true); 24 | return true; 25 | } else { 26 | done(false); 27 | return false; 28 | } 29 | }); 30 | } 31 | }); 32 | } catch (e) { 33 | if (window.Store.Chat.length === 0) return false; 34 | 35 | firstChat = Store.Chat.models[0]; 36 | var originalID = firstChat.id; 37 | firstChat.id = 38 | typeof originalID === 'string' 39 | ? id 40 | : new window.Store.UserConstructor(id, { 41 | intentionallyUsePrivateConstructor: true, 42 | }); 43 | if (done !== undefined) { 44 | firstChat.sendMessage(message).then(function () { 45 | firstChat.id = originalID; 46 | done(true); 47 | }); 48 | return true; 49 | } else { 50 | firstChat.sendMessage(message); 51 | firstChat.id = originalID; 52 | return true; 53 | } 54 | } 55 | if (done !== undefined) done(false); 56 | return false; 57 | } 58 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-message-with-tags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sends text message including @tag mentions 3 | * @param {string} to Chat id 4 | * @param {string} body Body message 5 | */ 6 | export async function sendMessageWithTags(to, body) { 7 | var chat = to.id ? to : Store.Chat.get(to); 8 | var chatId = chat.id._serialized; 9 | var msgIveSent = chat.msgs.filter((msg) => msg.__x_isSentByMe)[0]; 10 | if (!msgIveSent) { 11 | return chat.sendMessage(body); 12 | } 13 | 14 | var tempMsg = Object.create(msgIveSent); 15 | var newId = window.WAPI.getNewMessageId(chatId); 16 | var mentionedJidList = 17 | body 18 | .match(/@(\d*)/g) 19 | .map((x) => new Store.WidFactory.createUserWid(x.replace('@', ''))) || 20 | undefined; 21 | 22 | var extend = { 23 | ack: 0, 24 | id: newId, 25 | local: !0, 26 | self: 'out', 27 | t: parseInt(new Date().getTime() / 1000), 28 | to: new Store.WidFactory.createWid(chatId), 29 | isNewMsg: !0, 30 | type: 'chat', 31 | body, 32 | quotedMsg: null, 33 | mentionedJidList, 34 | }; 35 | 36 | Object.assign(tempMsg, extend); 37 | await Store.addAndSendMsgToChat(chat, tempMsg); 38 | return newId._serialized; 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-message-with-thumb.js: -------------------------------------------------------------------------------- 1 | export function sendMessageWithThumb( 2 | thumb, 3 | url, 4 | title, 5 | description, 6 | chatId, 7 | done 8 | ) { 9 | var chatSend = WAPI.getChat(chatId); 10 | if (chatSend === undefined) { 11 | if (done !== undefined) done(false); 12 | return false; 13 | } 14 | var linkPreview = { 15 | canonicalUrl: url, 16 | description: description, 17 | matchedText: url, 18 | title: title, 19 | thumbnail: thumb, 20 | }; 21 | chatSend.sendMessage(url, { 22 | linkPreview: linkPreview, 23 | mentionedJidList: [], 24 | quotedMsg: null, 25 | quotedMsgAdminGroupJid: null, 26 | }); 27 | if (done !== undefined) done(true); 28 | return true; 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-message.js: -------------------------------------------------------------------------------- 1 | export function sendMessage(id, message, done) { 2 | let chat = WAPI.getChat(id); 3 | function sleep(ms) { 4 | return new Promise((resolve) => setTimeout(resolve, ms)); 5 | } 6 | if (chat !== undefined) { 7 | if (done !== undefined) { 8 | chat.sendMessage(message).then(function () { 9 | let trials = 0; 10 | function check() { 11 | for (let i = chat.msgs.models.length - 1; i >= 0; i--) { 12 | let msg = chat.msgs.models[i]; 13 | 14 | if (!msg.senderObj.isMe || msg.body != message) { 15 | continue; 16 | } 17 | done(WAPI._serializeMessageObj(msg)); 18 | return True; 19 | } 20 | trials += 1; 21 | console.log(trials); 22 | if (trials > 30) { 23 | done(true); 24 | return; 25 | } 26 | sleep(500).then(check); 27 | } 28 | check(); 29 | }); 30 | return true; 31 | } else { 32 | return chat 33 | .sendMessage(message) 34 | .then((_) => chat.lastReceivedKey._serialized); 35 | } 36 | } else { 37 | if (done !== undefined) done(false); 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-message2.js: -------------------------------------------------------------------------------- 1 | export function sendMessage2(id, message, done) { 2 | var chat = WAPI.getChat(id); 3 | if (chat !== undefined) { 4 | try { 5 | if (done !== undefined) { 6 | chat.sendMessage(message).then(function () { 7 | done(true); 8 | }); 9 | } else { 10 | chat.sendMessage(message); 11 | } 12 | return true; 13 | } catch (error) { 14 | if (done !== undefined) done(false); 15 | return false; 16 | } 17 | } 18 | if (done !== undefined) done(false); 19 | return false; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-seen.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sends seens to given chat id 3 | * @param {string} id Chat id 4 | * @param {Function} done Optional callback 5 | */ 6 | export function sendSeen(id, done) { 7 | var chat = window.WAPI.getChat(id); 8 | if (chat !== undefined) { 9 | if (done !== undefined) { 10 | Store.SendSeen(chat, false).then(function () { 11 | done(true); 12 | }); 13 | return true; 14 | } else { 15 | Store.SendSeen(chat, false); 16 | return true; 17 | } 18 | } 19 | if (done !== undefined) done(); 20 | return false; 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-sticker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sends sticker, this method should be called with a helper like 'sendImageAsSticker' 3 | * @param {*} sticker Sticker 4 | * @param {string} chatId Chat id 5 | * @param {*} metadata sharp metadata (https://sharp.pixelplumbing.com/api-input#metadata) 6 | */ 7 | export async function sendSticker(sticker, chatId, metadata) { 8 | var chat = Store.Chat.get(chatId); 9 | let stick = new window.Store.Sticker.modelClass(); 10 | stick.__x_clientUrl = sticker.clientUrl; 11 | stick.__x_filehash = sticker.filehash; 12 | stick.__x_id = sticker.filehash; 13 | stick.__x_uploadhash = sticker.uploadhash; 14 | stick.__x_mediaKey = sticker.mediaKey; 15 | stick.__x_initialized = false; 16 | stick.__x_mediaData.mediaStage = 'INIT'; 17 | stick.mimetype = 'image/webp'; 18 | stick.height = metadata && metadata.height ? metadata.height : 512; 19 | stick.width = metadata && metadata.width ? metadata.width : 512; 20 | await stick.initialize(); 21 | return await stick.sendToChat(chat); 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/send-video-as-gif.js: -------------------------------------------------------------------------------- 1 | import { processFiles } from './process-files'; 2 | import { base64ToFile } from '../helper'; 3 | 4 | /** 5 | * Sends video as a gif to given chat id 6 | * @param {string} dataBase64 7 | * @param {string} chatid 8 | * @param {string} filename 9 | * @param {string} caption 10 | * @param {Function} done Optional callback 11 | */ 12 | export function sendVideoAsGif(dataBase64, chatid, filename, caption, done) { 13 | // const idUser = new window.Store.UserConstructor(chatid); 14 | const idUser = new Store.WidFactory.createWid(chatid); 15 | return Store.Chat.find(idUser).then((chat) => { 16 | var mediaBlob = base64ToFile(dataBase64, filename); 17 | processFiles(chat, mediaBlob).then((mc) => { 18 | var media = mc.models[0]; 19 | media.mediaPrep._mediaData.isGif = true; 20 | media.mediaPrep._mediaData.gifAttribution = 1; 21 | media.mediaPrep.sendToChat(chat, { caption: caption }); 22 | if (done !== undefined) done(true); 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/set-my-name.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sets current user profile name 3 | * @param {string} name 4 | */ 5 | export async function setMyName(name) { 6 | if (!Store.Versions.default[11].BinaryProtocol) { 7 | Store.Versions.default[11].BinaryProtocol = new Store.bp(11); 8 | } 9 | return await Store.Versions.default[11].setPushname(name); 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/set-my-status.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sets current user status 3 | * @param {string} status 4 | */ 5 | export function setMyStatus(status) { 6 | return Store.MyStatus.setMyStatus(status); 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/simulate-typing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Starts typing 3 | * @param {string} chatId 4 | */ 5 | export async function startTyping(chatId) { 6 | await Store.ChatStates.sendChatStateComposing(chatId); 7 | } 8 | 9 | /** 10 | * Stops typing 11 | * @param {string} chatId 12 | */ 13 | export async function stopTyping(chatId) { 14 | await Store.ChatStates.sendChatStatePaused(chatId); 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/wapi/functions/unblock-contact.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unblocks contact 3 | * @param {string} id Contact id (00000000@c.us) 4 | * @param {Function} done Optional callback 5 | */ 6 | export function unblockContact(id, done) { 7 | const contact = window.Store.Contact.get(id); 8 | if (contact !== undefined) { 9 | contact.setBlock(!1); 10 | done(true); 11 | return true; 12 | } 13 | done(false); 14 | return false; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/wapi/helper/array-buffer-to-base64.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Optimized Arraybuffer to Base64 converter 3 | * @param {ArrayBuffer} arrayBuffer 4 | */ 5 | export function arrayBufferToBase64(arrayBuffer) { 6 | var base64 = ''; 7 | var encodings = 8 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 9 | 10 | var bytes = new Uint8Array(arrayBuffer); 11 | var byteLength = bytes.byteLength; 12 | var byteRemainder = byteLength % 3; 13 | var mainLength = byteLength - byteRemainder; 14 | 15 | var a, b, c, d; 16 | var chunk; 17 | 18 | // Main loop deals with bytes in chunks of 3 19 | for (var i = 0; i < mainLength; i = i + 3) { 20 | // Combine the three bytes into a single integer 21 | chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; 22 | 23 | // Use bitmasks to extract 6-bit segments from the triplet 24 | a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18 25 | b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12 26 | c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 27 | d = chunk & 63; // 63 = 2^6 - 1 28 | 29 | // Convert the raw binary segments to the appropriate ASCII encoding 30 | base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; 31 | } 32 | 33 | // Deal with the remaining bytes and padding 34 | if (byteRemainder == 1) { 35 | chunk = bytes[mainLength]; 36 | 37 | a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 38 | 39 | // Set the 4 least significant bits to zero 40 | b = (chunk & 3) << 4; // 3 = 2^2 - 1 41 | 42 | base64 += encodings[a] + encodings[b] + '=='; 43 | } else if (byteRemainder == 2) { 44 | chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; 45 | 46 | a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10 47 | b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 48 | 49 | // Set the 2 least significant bits to zero 50 | c = (chunk & 15) << 2; // 15 = 2^4 - 1 51 | 52 | base64 += encodings[a] + encodings[b] + encodings[c] + '='; 53 | } 54 | return base64; 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/wapi/helper/base64-to-file.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts base64 string to a file 3 | * @returns The file 4 | * @param {string} base64 5 | * @param {string} filename 6 | */ 7 | export function base64ToFile(base64, filename) { 8 | var arr = base64.split(','); 9 | var mime = arr[0].match(/:(.*?);/)[1]; 10 | var bstr = atob(arr[1]); 11 | var n = bstr.length; 12 | var u8arr = new Uint8Array(n); 13 | 14 | while (n--) { 15 | u8arr[n] = bstr.charCodeAt(n); 16 | } 17 | 18 | return new File([u8arr], filename, { type: mime }); 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/wapi/helper/generate-media-key.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates media key 3 | * @param {number} length 4 | */ 5 | export function generateMediaKey(length) { 6 | let result = ''; 7 | let characters = 8 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 9 | for (let i = 0; i < length; i++) { 10 | result += characters.charAt(Math.floor(Math.random() * characters.length)); 11 | } 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/wapi/helper/get-file-hash.js: -------------------------------------------------------------------------------- 1 | import * as jsSHA from '../jssha'; 2 | 3 | /** 4 | * Retrieves given file hash (SHA-256 -> Base64) 5 | * @param {Blob} data 6 | */ 7 | export async function getFileHash(data) { 8 | let buffer = await data.arrayBuffer(); 9 | var sha = new jsSHA('SHA-256', 'ARRAYBUFFER'); 10 | sha.update(buffer); 11 | return sha.getHash('B64'); 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/wapi/helper/index.js: -------------------------------------------------------------------------------- 1 | export { base64ToFile } from './base64-to-file'; 2 | export { getFileHash } from './get-file-hash'; 3 | export { generateMediaKey } from './generate-media-key'; 4 | export { arrayBufferToBase64 } from './array-buffer-to-base64'; 5 | -------------------------------------------------------------------------------- /src/lib/wapi/helper/is-chat-message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @returns true if is a chat messae, false otherwise 3 | * @param {Message} message 4 | */ 5 | export function isChatMessage(message) { 6 | if (message.isSentByMe) { 7 | return false; 8 | } 9 | if (message.isNotification) { 10 | return false; 11 | } 12 | if (!message.isUserCreatedType) { 13 | return false; 14 | } 15 | return true; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/wapi/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6" 5 | }, 6 | } -------------------------------------------------------------------------------- /src/lib/wapi/listeners/add-all-new-messages.js: -------------------------------------------------------------------------------- 1 | export function allNewMessagesListener() { 2 | window.WAPI.allNewMessagesListener = (callback) => 3 | window.Store.Msg.on('add', (newMessage) => { 4 | if (newMessage && newMessage.isNewMsg) { 5 | let message = window.WAPI.processMessageObj(newMessage, true, false); 6 | if (message) { 7 | callback(message); 8 | } 9 | } 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/wapi/listeners/add-new-messages.js: -------------------------------------------------------------------------------- 1 | export function addNewMessagesListener() { 2 | window.WAPI.waitNewMessages = waitNewMessages; 3 | } 4 | 5 | /** 6 | * Registers a callback to be called when a new message arrives the WAPI. 7 | * @param rmCallbackAfterUse - Boolean - Specify if the callback need to be executed only once 8 | * @param done - function - Callback function to be called when a new message arrives. 9 | * @returns {boolean} 10 | */ 11 | function waitNewMessages(rmCallbackAfterUse = true, done) { 12 | window.WAPI._newMessagesCallbacks.push({ 13 | callback: done, 14 | rmAfterUse: rmCallbackAfterUse, 15 | }); 16 | return true; 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/wapi/listeners/add-on-added-to-group.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Registers listener for the device when is added to a group 3 | */ 4 | export function addOnAddedToGroup() { 5 | /** 6 | * Registers a callback that fires when your host phone is added to a group. 7 | * @param callback - function - Callback function to be called when a message acknowledgement changes. The callback returns 3 variables 8 | * @returns {boolean} 9 | */ 10 | window.WAPI.onAddedToGroup = function (callback) { 11 | Store.Chat.on('add', (chatObject) => { 12 | if (chatObject && chatObject.isGroup) { 13 | callback(chatObject); 14 | } 15 | }); 16 | return true; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/wapi/listeners/add-on-live-location.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Regiters on live location listener 3 | */ 4 | export function addOnLiveLocation() { 5 | window.WAPI.onLiveLocation = function (chatId, callback) { 6 | var lLChat = Store.LiveLocation.get(chatId); 7 | if (lLChat) { 8 | var validLocs = lLChat.participants.validLocations(); 9 | validLocs.map((x) => 10 | x.on('change:lastUpdated', (x, y, z) => { 11 | console.log(x, y, z); 12 | const { id, lat, lng, accuracy, degrees, speed, lastUpdated } = x; 13 | const l = { 14 | id: id.toString(), 15 | lat, 16 | lng, 17 | accuracy, 18 | degrees, 19 | speed, 20 | lastUpdated, 21 | }; 22 | // console.log('newloc',l) 23 | callback(l); 24 | }) 25 | ); 26 | return true; 27 | } else { 28 | return false; 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/wapi/listeners/add-on-new-ack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Registers a callback to be called when a the acknowledgement state of a message changes. 3 | * @param callback - function - Callback function to be called when a message acknowledgement changes. 4 | * @returns {boolean} 5 | */ 6 | export function addOnNewAcks() { 7 | window.WAPI.waitNewAcknowledgements = function (callback) { 8 | Store.Msg.on('change:ack', callback); 9 | return true; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/wapi/listeners/add-on-participants-change.js: -------------------------------------------------------------------------------- 1 | let groupParticpiantsEvents = {}; 2 | 3 | /** 4 | * Registers on participants change listener 5 | */ 6 | export function addOnParticipantsChange() { 7 | /** 8 | * Registers a callback to participant changes on a certain, specific group 9 | * @param groupId - string - The id of the group that you want to attach the callback to. 10 | * @param callback - function - Callback function to be called when a message acknowledgement changes. The callback returns 3 variables 11 | * @returns {boolean} 12 | */ 13 | window.WAPI.onParticipantsChanged = function (groupId, callback) { 14 | const subtypeEvents = [ 15 | 'invite', 16 | 'add', 17 | 'remove', 18 | 'leave', 19 | 'promote', 20 | 'demote', 21 | ]; 22 | const chat = window.Store.Chat.get(groupId); 23 | //attach all group Participants to the events object as 'add' 24 | const metadata = window.Store.GroupMetadata.get(groupId); 25 | if (!groupParticpiantsEvents[groupId]) { 26 | groupParticpiantsEvents[groupId] = {}; 27 | metadata.participants.forEach((participant) => { 28 | groupParticpiantsEvents[groupId][participant.id.toString()] = { 29 | subtype: 'add', 30 | from: metadata.owner, 31 | }; 32 | }); 33 | } 34 | let i = 0; 35 | chat.on('change:groupMetadata.participants', (_) => 36 | chat.on('all', (x, y) => { 37 | const { isGroup, previewMessage } = y; 38 | if ( 39 | isGroup && 40 | x === 'change' && 41 | previewMessage && 42 | previewMessage.type === 'gp2' && 43 | subtypeEvents.includes(previewMessage.subtype) 44 | ) { 45 | const { subtype, from, recipients } = previewMessage; 46 | const rec = recipients[0].toString(); 47 | if ( 48 | groupParticpiantsEvents[groupId][rec] && 49 | groupParticpiantsEvents[groupId][recipients[0]].subtype == subtype 50 | ) { 51 | //ignore, this is a duplicate entry 52 | // console.log('duplicate event') 53 | } else { 54 | //ignore the first message 55 | if (i == 0) { 56 | //ignore it, plus 1, 57 | i++; 58 | } else { 59 | groupParticpiantsEvents[groupId][rec] = { subtype, from }; 60 | //fire the callback 61 | // // previewMessage.from.toString() 62 | // x removed y 63 | // x added y 64 | callback({ 65 | by: from.toString(), 66 | action: subtype, 67 | who: recipients, 68 | }); 69 | chat.off('all', this); 70 | i = 0; 71 | } 72 | } 73 | } 74 | }) 75 | ); 76 | return true; 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/lib/wapi/listeners/add-on-state-change.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Registers a callback for state change event 3 | * It could be [UNLAUNCHED, OPENING, PAIRING, CONNECTED, TIMEOUT] 4 | * @param callback - function - Callback function to be called when the device state changes 5 | */ 6 | 7 | export function addOnStateChange() { 8 | window.WAPI.onStateChange = function (callback) { 9 | window.Store.State.default.on('change:state', callback); 10 | return true; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/wapi/listeners/index.js: -------------------------------------------------------------------------------- 1 | export { initNewMessagesListener } from './init-listeners'; 2 | export { addNewMessagesListener } from './add-new-messages'; 3 | export { allNewMessagesListener } from './add-all-new-messages'; 4 | export { addOnStateChange } from './add-on-state-change'; 5 | export { addOnNewAcks } from './add-on-new-ack'; 6 | export { addOnLiveLocation } from './add-on-live-location'; 7 | export { addOnParticipantsChange } from './add-on-participants-change'; 8 | export { addOnAddedToGroup } from './add-on-added-to-group'; 9 | -------------------------------------------------------------------------------- /src/lib/wapi/listeners/init-common-listener.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This needs to be executed before new adding new message listeners 3 | */ 4 | export function initNewMessagesListener() { 5 | window.WAPI._newMessagesListener = window.Store.Msg.on( 6 | 'add', 7 | (newMessage) => { 8 | if (newMessage && newMessage.isNewMsg && !newMessage.isSentByMe) { 9 | let message = window.WAPI.processMessageObj(newMessage, false, false); 10 | if (message) { 11 | window.WAPI._newMessagesQueue.push(message); 12 | window.WAPI._newMessagesBuffer.push(message); 13 | } 14 | 15 | // Starts debouncer time to don't call a callback for each message if more than one message arrives 16 | // in the same second 17 | if ( 18 | !window.WAPI._newMessagesDebouncer && 19 | window.WAPI._newMessagesQueue.length > 0 20 | ) { 21 | window.WAPI._newMessagesDebouncer = setTimeout(() => { 22 | let queuedMessages = window.WAPI._newMessagesQueue; 23 | 24 | window.WAPI._newMessagesDebouncer = null; 25 | window.WAPI._newMessagesQueue = []; 26 | 27 | let removeCallbacks = []; 28 | 29 | window.WAPI._newMessagesCallbacks.forEach(function (callbackObj) { 30 | if (callbackObj.callback !== undefined) { 31 | callbackObj.callback(queuedMessages); 32 | } 33 | if (callbackObj.rmAfterUse === true) { 34 | removeCallbacks.push(callbackObj); 35 | } 36 | }); 37 | 38 | // Remove removable callbacks. 39 | removeCallbacks.forEach(function (rmCallbackObj) { 40 | let callbackIndex = window.WAPI._newMessagesCallbacks.indexOf( 41 | rmCallbackObj 42 | ); 43 | window.WAPI._newMessagesCallbacks.splice(callbackIndex, 1); 44 | }); 45 | }, 1000); 46 | } 47 | } 48 | } 49 | ); 50 | 51 | window.WAPI._unloadInform = (event) => { 52 | // Save in the buffer the ungot unreaded messages 53 | window.WAPI._newMessagesBuffer.forEach((message) => { 54 | Object.keys(message).forEach((key) => 55 | message[key] === undefined ? delete message[key] : '' 56 | ); 57 | }); 58 | sessionStorage.setItem( 59 | 'saved_msgs', 60 | JSON.stringify(window.WAPI._newMessagesBuffer) 61 | ); 62 | 63 | // Inform callbacks that the page will be reloaded. 64 | window.WAPI._newMessagesCallbacks.forEach(function (callbackObj) { 65 | if (callbackObj.callback !== undefined) { 66 | callbackObj.callback({ 67 | status: -1, 68 | message: 'page will be reloaded, wait and register callback again.', 69 | }); 70 | } 71 | }); 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/lib/wapi/listeners/init-listeners.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This needs to be executed before new adding new message listeners 3 | */ 4 | export function initNewMessagesListener() { 5 | window.WAPI._newMessagesListener = window.Store.Msg.on( 6 | 'add', 7 | (newMessage) => { 8 | if (newMessage && newMessage.isNewMsg && !newMessage.isSentByMe) { 9 | let message = window.WAPI.processMessageObj(newMessage, false, false); 10 | if (message) { 11 | window.WAPI._newMessagesQueue.push(message); 12 | window.WAPI._newMessagesBuffer.push(message); 13 | } 14 | 15 | // Starts debouncer time to don't call a callback for each message if more than one message arrives 16 | // in the same second 17 | if ( 18 | !window.WAPI._newMessagesDebouncer && 19 | window.WAPI._newMessagesQueue.length > 0 20 | ) { 21 | window.WAPI._newMessagesDebouncer = setTimeout(() => { 22 | let queuedMessages = window.WAPI._newMessagesQueue; 23 | 24 | window.WAPI._newMessagesDebouncer = null; 25 | window.WAPI._newMessagesQueue = []; 26 | 27 | let removeCallbacks = []; 28 | 29 | window.WAPI._newMessagesCallbacks.forEach(function (callbackObj) { 30 | if (callbackObj.callback !== undefined) { 31 | callbackObj.callback(queuedMessages); 32 | } 33 | if (callbackObj.rmAfterUse === true) { 34 | removeCallbacks.push(callbackObj); 35 | } 36 | }); 37 | 38 | // Remove removable callbacks. 39 | removeCallbacks.forEach(function (rmCallbackObj) { 40 | let callbackIndex = window.WAPI._newMessagesCallbacks.indexOf( 41 | rmCallbackObj 42 | ); 43 | window.WAPI._newMessagesCallbacks.splice(callbackIndex, 1); 44 | }); 45 | }, 1000); 46 | } 47 | } 48 | } 49 | ); 50 | 51 | window.WAPI._unloadInform = (event) => { 52 | // Save in the buffer the ungot unreaded messages 53 | window.WAPI._newMessagesBuffer.forEach((message) => { 54 | Object.keys(message).forEach((key) => 55 | message[key] === undefined ? delete message[key] : '' 56 | ); 57 | }); 58 | sessionStorage.setItem( 59 | 'saved_msgs', 60 | JSON.stringify(window.WAPI._newMessagesBuffer) 61 | ); 62 | 63 | // Inform callbacks that the page will be reloaded. 64 | window.WAPI._newMessagesCallbacks.forEach(function (callbackObj) { 65 | if (callbackObj.callback !== undefined) { 66 | callbackObj.callback({ 67 | status: -1, 68 | message: 'page will be reloaded, wait and register callback again.', 69 | }); 70 | } 71 | }); 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/lib/wapi/serializers/index.js: -------------------------------------------------------------------------------- 1 | export { _serializeChatObj } from './serialize-chat'; 2 | export { _serializeRawObj } from './serialize-raw'; 3 | export { _serializeMessageObj } from './serialize-message'; 4 | export { _serializeContactObj } from './serialize-contact'; 5 | export { _serializeNumberStatusObj } from './serialize-number-status'; 6 | export { _serializeProfilePicThumb } from './serialize-profile-pic-thumb'; -------------------------------------------------------------------------------- /src/lib/wapi/serializers/serialize-chat.js: -------------------------------------------------------------------------------- 1 | import { _serializeRawObj } from './serialize-raw'; 2 | 3 | /** 4 | * Serializes a chat object 5 | * 6 | * @param rawChat Chat object 7 | * @returns {Chat} 8 | */ 9 | export const _serializeChatObj = (obj) => { 10 | if (obj == undefined) { 11 | return null; 12 | } 13 | return Object.assign(window.WAPI._serializeRawObj(obj), { 14 | kind: obj.kind, 15 | isGroup: obj.isGroup, 16 | contact: obj['contact'] 17 | ? window.WAPI._serializeContactObj(obj['contact']) 18 | : null, 19 | groupMetadata: obj['groupMetadata'] 20 | ? window.WAPI._serializeRawObj(obj['groupMetadata']) 21 | : null, 22 | presence: obj['presence'] 23 | ? window.WAPI._serializeRawObj(obj['presence']) 24 | : null, 25 | msgs: null, 26 | isOnline: obj.__x_presence.attributes.isOnline || null, 27 | lastSeen: 28 | obj && 29 | obj.previewMessage && 30 | obj.previewMessage.__x_ephemeralStartTimestamp 31 | ? obj.previewMessage.__x_ephemeralStartTimestamp * 1000 32 | : null, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/wapi/serializers/serialize-contact.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serializes contact object 3 | * @param {Contact} obj 4 | */ 5 | export const _serializeContactObj = (obj) => { 6 | if (obj == undefined) { 7 | return null; 8 | } 9 | return Object.assign(window.WAPI._serializeRawObj(obj), { 10 | formattedName: obj.formattedName, 11 | isHighLevelVerified: obj.isHighLevelVerified, 12 | isMe: obj.isMe, 13 | isMyContact: obj.isMyContact, 14 | isPSA: obj.isPSA, 15 | isUser: obj.isUser, 16 | isVerified: obj.isVerified, 17 | isWAContact: obj.isWAContact, 18 | profilePicThumbObj: obj.profilePicThumb 19 | ? WAPI._serializeProfilePicThumb(obj.profilePicThumb) 20 | : {}, 21 | statusMute: obj.statusMute, 22 | msgs: null, 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/wapi/serializers/serialize-message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serializes chat object 3 | * @param {Chat} obj 4 | */ 5 | export const _serializeMessageObj = (obj) => { 6 | if (obj == undefined) { 7 | return null; 8 | } 9 | const _chat = WAPI._serializeChatObj(obj['chat']); 10 | if (obj.quotedMsg) obj.quotedMsgObj(); 11 | return Object.assign(window.WAPI._serializeRawObj(obj), { 12 | id: obj.id._serialized, 13 | sender: obj['senderObj'] 14 | ? WAPI._serializeContactObj(obj['senderObj']) 15 | : null, 16 | timestamp: obj['t'], 17 | content: obj['body'], 18 | isGroupMsg: obj.isGroupMsg, 19 | isLink: obj.isLink, 20 | isMMS: obj.isMMS, 21 | isMedia: obj.isMedia, 22 | isNotification: obj.isNotification, 23 | isPSA: obj.isPSA, 24 | type: obj.type, 25 | chat: _chat, 26 | isOnline: _chat.isOnline, 27 | lastSeen: _chat.lastSeen, 28 | chatId: obj.id.remote, 29 | quotedMsgObj: WAPI._serializeMessageObj(obj['_quotedMsgObj']), 30 | mediaData: window.WAPI._serializeRawObj(obj['mediaData']), 31 | reply: (body) => window.WAPI.reply(_chat.id._serialized, body, obj), 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /src/lib/wapi/serializers/serialize-number-status.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serializes number status object 3 | * @param {*} obj 4 | */ 5 | export const _serializeNumberStatusObj = (obj) => { 6 | if (obj == undefined) { 7 | return null; 8 | } 9 | 10 | return Object.assign( 11 | {}, 12 | { 13 | id: obj.jid, 14 | status: obj.status, 15 | isBusiness: obj.biz === true, 16 | canReceiveMessage: obj.status === 200, 17 | } 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/wapi/serializers/serialize-profile-pic-thumb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serializes profile pic (thumbnail) object 3 | * @param {*} obj 4 | */ 5 | export const _serializeProfilePicThumb = (obj) => { 6 | if (obj == undefined) { 7 | return null; 8 | } 9 | 10 | return Object.assign( 11 | {}, 12 | { 13 | eurl: obj.eurl, 14 | id: obj.id, 15 | img: obj.img, 16 | imgFull: obj.imgFull, 17 | raw: obj.raw, 18 | tag: obj.tag, 19 | } 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/lib/wapi/serializers/serialize-raw.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Serializes object into JSON format safely 3 | * @param {*} obj 4 | */ 5 | export const _serializeRawObj = (obj) => { 6 | if (obj && obj.toJSON) { 7 | return obj.toJSON(); 8 | } 9 | return {}; 10 | }; 11 | -------------------------------------------------------------------------------- /src/lib/wapi/store/get-store.js: -------------------------------------------------------------------------------- 1 | import { storeObjects } from './store-objects'; 2 | 3 | export function getStore(modules) { 4 | let foundCount = 0; 5 | let neededObjects = storeObjects; 6 | for (let idx in modules) { 7 | if (typeof modules[idx] === 'object' && modules[idx] !== null) { 8 | let first = Object.values(modules[idx])[0]; 9 | if (typeof first === 'object' && first.exports) { 10 | for (let idx2 in modules[idx]) { 11 | let module = modules(idx2); 12 | if (!module) { 13 | continue; 14 | } 15 | neededObjects.forEach((needObj) => { 16 | if (!needObj.conditions || needObj.foundedModule) return; 17 | let neededModule = needObj.conditions(module); 18 | if (neededModule !== null) { 19 | foundCount++; 20 | needObj.foundedModule = neededModule; 21 | } 22 | }); 23 | if (foundCount == neededObjects.length) { 24 | break; 25 | } 26 | } 27 | 28 | let neededStore = neededObjects.find( 29 | (needObj) => needObj.id === 'Store' 30 | ); 31 | window.Store = neededStore.foundedModule 32 | ? neededStore.foundedModule 33 | : {}; 34 | neededObjects.splice(neededObjects.indexOf(neededStore), 1); 35 | neededObjects.forEach((needObj) => { 36 | if (needObj.foundedModule) { 37 | window.Store[needObj.id] = needObj.foundedModule; 38 | } 39 | }); 40 | window.Store.sendMessage = function (e) { 41 | return window.Store.SendTextMsgToChat(this, ...arguments); 42 | }; 43 | if (window.Store.MediaCollection) 44 | window.Store.MediaCollection.prototype.processFiles = 45 | window.Store.MediaCollection.prototype.processFiles || 46 | window.Store.MediaCollection.prototype.processAttachments; 47 | return window.Store; 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/wapi/store/store-objects.js: -------------------------------------------------------------------------------- 1 | export const storeObjects = [ 2 | { 3 | id: 'Store', 4 | conditions: (module) => 5 | module && module.default && module.default.Chat && module.default.Msg 6 | ? module.default 7 | : null, 8 | }, 9 | { 10 | id: 'MediaCollection', 11 | conditions: (module) => 12 | module && module.default && 13 | module.default.prototype && 14 | (module.default.prototype.processFiles !== undefined || 15 | module.default.prototype.processAttachments !== undefined) 16 | ? module.default 17 | : null, 18 | }, 19 | { id: 'MediaProcess', conditions: (module) => (module.BLOB ? module : null) }, 20 | { 21 | id: 'Archive', 22 | conditions: (module) => (module.setArchive ? module : null), 23 | }, 24 | { 25 | id: 'Block', 26 | conditions: (module) => 27 | module.blockContact && module.unblockContact ? module : null, 28 | }, 29 | { 30 | id: 'ChatUtil', 31 | conditions: (module) => (module.sendClear ? module : null), 32 | }, 33 | { 34 | id: 'GroupInvite', 35 | conditions: (module) => (module.queryGroupInviteCode ? module : null), 36 | }, 37 | { id: 'Wap', conditions: (module) => (module.createGroup ? module : null) }, 38 | { 39 | id: 'ServiceWorker', 40 | conditions: (module) => 41 | module && module.default && module.default.killServiceWorker ? module : null, 42 | }, 43 | { 44 | id: 'State', 45 | conditions: (module) => (module.STATE && module.STREAM ? module : null), 46 | }, 47 | { 48 | id: '_Presence', 49 | conditions: (module) => 50 | module.setPresenceAvailable && module.setPresenceUnavailable 51 | ? module 52 | : null, 53 | }, 54 | { 55 | id: 'WapDelete', 56 | conditions: (module) => 57 | module.sendConversationDelete && module.sendConversationDelete.length == 2 58 | ? module 59 | : null, 60 | }, 61 | { 62 | id: 'Conn', 63 | conditions: (module) => 64 | module && module.default && module.default.ref && module.default.refTTL 65 | ? module.default 66 | : null, 67 | }, 68 | { 69 | id: 'WapQuery', 70 | conditions: (module) => 71 | module.queryExist 72 | ? module 73 | : module && module.default && module.default.queryExist 74 | ? module && module.default 75 | : null, 76 | }, 77 | { 78 | id: 'CryptoLib', 79 | conditions: (module) => (module.decryptE2EMedia ? module : null), 80 | }, 81 | { 82 | id: 'OpenChat', 83 | conditions: (module) => 84 | module && module.default && 85 | module.default.prototype && 86 | module.default.prototype.openChat 87 | ? module.default 88 | : null, 89 | }, 90 | { 91 | id: 'UserConstructor', 92 | conditions: (module) => 93 | module && module.default && 94 | module.default.prototype && 95 | module.default.prototype.isServer && 96 | module.default.prototype.isUser 97 | ? module.default 98 | : null, 99 | }, 100 | { 101 | id: 'SendTextMsgToChat', 102 | conditions: (module) => 103 | module.sendTextMsgToChat ? module.sendTextMsgToChat : null, 104 | }, 105 | { id: 'ReadSeen', conditions: (module) => (module.sendSeen ? module : null) }, 106 | { 107 | id: 'sendDelete', 108 | conditions: (module) => (module.sendDelete ? module.sendDelete : null), 109 | }, 110 | { 111 | id: 'addAndSendMsgToChat', 112 | conditions: (module) => 113 | module.addAndSendMsgToChat ? module.addAndSendMsgToChat : null, 114 | }, 115 | { 116 | id: 'sendMsgToChat', 117 | conditions: (module) => 118 | module.sendMsgToChat ? module.sendMsgToChat : null, 119 | }, 120 | { 121 | id: 'Catalog', 122 | conditions: (module) => (module.Catalog ? module.Catalog : null), 123 | }, 124 | { 125 | id: 'bp', 126 | conditions: (module) => 127 | module && module.default && 128 | module.default.toString && 129 | module.default.toString().includes('bp_unknown_version') 130 | ? module.default 131 | : null, 132 | }, 133 | { 134 | id: 'MsgKey', 135 | conditions: (module) => 136 | module && module.default && 137 | module.default.toString().includes('MsgKey error: obj is null/undefined') 138 | ? module.default 139 | : null, 140 | }, 141 | { 142 | id: 'Parser', 143 | conditions: (module) => 144 | module.convertToTextWithoutSpecialEmojis ? module.default : null, 145 | }, 146 | { 147 | id: 'Builders', 148 | conditions: (module) => 149 | module.TemplateMessage && module.HydratedFourRowTemplate ? module : null, 150 | }, 151 | { 152 | id: 'Me', 153 | conditions: (module) => 154 | module.PLATFORMS && module.Conn ? module.default : null, 155 | }, 156 | { 157 | id: 'CallUtils', 158 | conditions: (module) => 159 | module.sendCallEnd && module.parseCall ? module : null, 160 | }, 161 | { 162 | id: 'Identity', 163 | conditions: (module) => 164 | module.queryIdentity && module.updateIdentity ? module : null, 165 | }, 166 | { 167 | id: 'MyStatus', 168 | conditions: (module) => 169 | module.getStatus && module.setMyStatus ? module : null, 170 | }, 171 | { 172 | id: 'ChatStates', 173 | conditions: (module) => 174 | module.sendChatStatePaused && 175 | module.sendChatStateRecording && 176 | module.sendChatStateComposing 177 | ? module 178 | : null, 179 | }, 180 | { 181 | id: 'GroupActions', 182 | conditions: (module) => 183 | module.sendExitGroup && module.localExitGroup ? module : null, 184 | }, 185 | { 186 | id: 'Features', 187 | conditions: (module) => 188 | module.FEATURE_CHANGE_EVENT && module.features ? module : null, 189 | }, 190 | { 191 | id: 'MessageUtils', 192 | conditions: (module) => 193 | module.storeMessages && module.appendMessage ? module : null, 194 | }, 195 | { 196 | id: 'WebMessageInfo', 197 | conditions: (module) => 198 | module.WebMessageInfo && module.WebFeatures 199 | ? module.WebMessageInfo 200 | : null, 201 | }, 202 | { 203 | id: 'createMessageKey', 204 | conditions: (module) => 205 | module.createMessageKey && module.createDeviceSentMessage 206 | ? module.createMessageKey 207 | : null, 208 | }, 209 | { 210 | id: 'Participants', 211 | conditions: (module) => 212 | module.addParticipants && 213 | module.removeParticipants && 214 | module.promoteParticipants && 215 | module.demoteParticipants 216 | ? module 217 | : null, 218 | }, 219 | { 220 | id: 'WidFactory', 221 | conditions: (module) => 222 | module.isWidlike && module.createWid && module.createWidFromWidLike 223 | ? module 224 | : null, 225 | }, 226 | { 227 | id: 'Base', 228 | conditions: (module) => 229 | module.setSubProtocol && module.binSend && module.actionNode 230 | ? module 231 | : null, 232 | }, 233 | { 234 | id: 'Versions', 235 | conditions: (module) => 236 | module.loadProtoVersions && 237 | module.default['15'] && 238 | module.default['16'] && 239 | module.default['17'] 240 | ? module 241 | : null, 242 | }, 243 | { 244 | id: 'Sticker', 245 | conditions: (module) => 246 | module && module.default && module.default.Sticker ? module.default.Sticker : null, 247 | }, 248 | { 249 | id: 'MediaUpload', 250 | conditions: (module) => 251 | module && module.default && module.default.mediaUpload ? module.default : null, 252 | }, 253 | { 254 | id: 'UploadUtils', 255 | conditions: (module) => 256 | module && module.default && module.default.encryptAndUpload ? module.default : null, 257 | }, 258 | ]; 259 | -------------------------------------------------------------------------------- /src/lib/wapi/wapi.js: -------------------------------------------------------------------------------- 1 | import { 2 | _getGroupParticipants, 3 | addParticipant, 4 | areAllMessagesLoaded, 5 | blockContact, 6 | clearChat, 7 | createGroup, 8 | deleteConversation, 9 | deleteMessages, 10 | demoteParticipant, 11 | downloadFileWithCredentials, 12 | encryptAndUploadFile, 13 | forwardMessages, 14 | getAllChatIds, 15 | getAllChats, 16 | getAllChatsWithMessages, 17 | getAllChatsWithNewMessages, 18 | getAllContacts, 19 | getAllGroupMetadata, 20 | getAllGroups, 21 | getAllMessagesInChat, 22 | getAllNewMessages, 23 | getAllUnreadMessages, 24 | getBatteryLevel, 25 | getChat, 26 | getChatById, 27 | getChatByName, 28 | getCommonGroups, 29 | getContact, 30 | getGroupAdmins, 31 | getGroupInviteLink, 32 | getGroupMetadata, 33 | getGroupParticipantIDs, 34 | getHost, 35 | getMe, 36 | getMessageById, 37 | getMyContacts, 38 | getNewId, 39 | getNewMessageId, 40 | getNumberProfile, 41 | getProfilePicFromServer, 42 | getStatus, 43 | getUnreadMessages, 44 | getUnreadMessagesInChat, 45 | hasUndreadMessages, 46 | isConnected, 47 | isLoggedIn, 48 | leaveGroup, 49 | loadAllEarlierMessages, 50 | loadAndGetAllMessagesInChat, 51 | loadChatEarlierMessages, 52 | loadEarlierMessagesTillDate, 53 | processFiles, 54 | processMessageObj, 55 | promoteParticipant, 56 | removeParticipant, 57 | reply, 58 | revokeGroupInviteLink, 59 | sendChatstate, 60 | sendContact, 61 | sendFile, 62 | sendImage, 63 | sendImageAsSticker, 64 | sendImageWithProduct, 65 | sendLocation, 66 | sendMessage, 67 | sendMessage2, 68 | sendMessageToID, 69 | sendMessageWithTags, 70 | sendMessageWithThumb, 71 | sendSeen, 72 | sendSticker, 73 | sendVideoAsGif, 74 | setMyName, 75 | setMyStatus, 76 | startTyping, 77 | stopTyping, 78 | unblockContact, 79 | } from './functions'; 80 | import { 81 | base64ToFile, 82 | generateMediaKey, 83 | getFileHash, 84 | arrayBufferToBase64, 85 | } from './helper'; 86 | import { 87 | addNewMessagesListener, 88 | addOnAddedToGroup, 89 | addOnLiveLocation, 90 | addOnNewAcks, 91 | addOnParticipantsChange, 92 | addOnStateChange, 93 | allNewMessagesListener, 94 | initNewMessagesListener, 95 | } from './listeners'; 96 | import { 97 | _serializeChatObj, 98 | _serializeContactObj, 99 | _serializeMessageObj, 100 | _serializeNumberStatusObj, 101 | _serializeProfilePicThumb, 102 | _serializeRawObj, 103 | } from './serializers'; 104 | import { getStore } from './store/get-store'; 105 | 106 | if (!window.Store || !window.Store.Msg) { 107 | (function () { 108 | const parasite = `parasite${Date.now()}`; 109 | // webpackJsonp([], { [parasite]: (x, y, z) => getStore(z) }, [parasite]); 110 | if (typeof webpackJsonp === 'function') 111 | webpackJsonp([], { [parasite]: (x, y, z) => getStore(z) }, [parasite]); 112 | else 113 | webpackJsonp.push([ 114 | [parasite], 115 | { [parasite]: (x, y, z) => getStore(z) }, 116 | [[parasite]], 117 | ]); 118 | })(); 119 | } 120 | 121 | window.WAPI = { 122 | lastRead: {}, 123 | }; 124 | 125 | // Serializers assignations 126 | window.WAPI._serializeRawObj = _serializeRawObj; 127 | window.WAPI._serializeChatObj = _serializeChatObj; 128 | window.WAPI._serializeContactObj = _serializeContactObj; 129 | window.WAPI._serializeMessageObj = _serializeMessageObj; 130 | window.WAPI._serializeNumberStatusObj = _serializeNumberStatusObj; 131 | window.WAPI._serializeProfilePicThumb = _serializeProfilePicThumb; 132 | 133 | // Group Functions 134 | window.WAPI.createGroup = createGroup; 135 | window.WAPI.leaveGroup = leaveGroup; 136 | window.WAPI.revokeGroupInviteLink = revokeGroupInviteLink; 137 | window.WAPI.getGroupInviteLink = getGroupInviteLink; 138 | window.WAPI.getGroupAdmins = getGroupAdmins; 139 | window.WAPI.removeParticipant = removeParticipant; 140 | window.WAPI.addParticipant = addParticipant; 141 | window.WAPI.promoteParticipant = promoteParticipant; 142 | window.WAPI.demoteParticipant = demoteParticipant; 143 | 144 | // Chatting functions 145 | window.WAPI.sendChatstate = sendChatstate; 146 | window.WAPI.sendMessageWithThumb = sendMessageWithThumb; 147 | window.WAPI.processMessageObj = processMessageObj; 148 | window.WAPI.sendMessageWithTags = sendMessageWithTags; 149 | window.WAPI.sendMessage = sendMessage; 150 | window.WAPI.sendMessage2 = sendMessage2; 151 | window.WAPI.sendSeen = sendSeen; 152 | window.WAPI.deleteConversation = deleteConversation; 153 | window.WAPI.deleteMessages = deleteMessages; 154 | window.WAPI.clearChat = clearChat; 155 | window.WAPI.sendMessageToID = sendMessageToID; 156 | window.WAPI.sendImage = sendImage; 157 | window.WAPI.sendFile = sendFile; 158 | window.WAPI.setMyName = setMyName; 159 | window.WAPI.setMyStatus = setMyStatus; 160 | window.WAPI.sendVideoAsGif = sendVideoAsGif; 161 | window.WAPI.processFiles = processFiles; 162 | window.WAPI.sendImageWithProduct = sendImageWithProduct; 163 | window.WAPI.sendContact = sendContact; 164 | window.WAPI.forwardMessages = forwardMessages; 165 | window.WAPI.reply = reply; 166 | window.WAPI._sendSticker = sendSticker; 167 | window.WAPI.encryptAndUploadFile = encryptAndUploadFile; 168 | window.WAPI.sendImageAsSticker = sendImageAsSticker; 169 | window.WAPI.startTyping = startTyping; 170 | window.WAPI.stopTyping = stopTyping; 171 | window.WAPI.sendLocation = sendLocation; 172 | window.WAPI.blockContact = blockContact; 173 | window.WAPI.unblockContact = unblockContact; 174 | 175 | // Retrieving functions 176 | window.WAPI.getAllContacts = getAllContacts; 177 | window.WAPI.getMyContacts = getMyContacts; 178 | window.WAPI.getContact = getContact; 179 | window.WAPI.getAllChats = getAllChats; 180 | window.WAPI.haveNewMsg = hasUndreadMessages; 181 | window.WAPI.getAllChatsWithNewMsg = getAllChatsWithNewMessages; 182 | window.WAPI.getAllChatIds = getAllChatIds; 183 | window.WAPI.getAllNewMessages = getAllNewMessages; 184 | window.WAPI.getAllUnreadMessages = getAllUnreadMessages; 185 | window.WAPI.getAllChatsWithMessages = getAllChatsWithMessages; 186 | window.WAPI.getAllGroups = getAllGroups; 187 | window.WAPI.getChat = getChat; 188 | window.WAPI.getStatus = getStatus; 189 | window.WAPI.getChatByName = getChatByName; 190 | window.WAPI.getNewId = getNewId; 191 | window.WAPI.getChatById = getChatById; 192 | window.WAPI.getUnreadMessagesInChat = getUnreadMessagesInChat; 193 | window.WAPI.loadEarlierMessages = loadChatEarlierMessages; 194 | window.WAPI.loadAllEarlierMessages = loadAllEarlierMessages; 195 | window.WAPI.areAllMessagesLoaded = areAllMessagesLoaded; 196 | window.WAPI.loadEarlierMessagesTillDate = loadEarlierMessagesTillDate; 197 | window.WAPI.getAllGroupMetadata = getAllGroupMetadata; 198 | window.WAPI.getGroupMetadata = getGroupMetadata; 199 | window.WAPI._getGroupParticipants = _getGroupParticipants; 200 | window.WAPI.getGroupParticipantIDs = getGroupParticipantIDs; 201 | window.WAPI.getAllMessagesInChat = getAllMessagesInChat; 202 | window.WAPI.loadAndGetAllMessagesInChat = loadAndGetAllMessagesInChat; 203 | window.WAPI.getUnreadMessages = getUnreadMessages; 204 | window.WAPI.getCommonGroups = getCommonGroups; 205 | window.WAPI.getProfilePicFromServer = getProfilePicFromServer; 206 | window.WAPI.downloadFileWithCredentials = downloadFileWithCredentials; 207 | window.WAPI.getNumberProfile = getNumberProfile; 208 | window.WAPI.getMessageById = getMessageById; 209 | window.WAPI.getNewMessageId = getNewMessageId; 210 | window.WAPI.getFileHash = getFileHash; 211 | window.WAPI.generateMediaKey = generateMediaKey; 212 | window.WAPI.arrayBufferToBase64 = arrayBufferToBase64; 213 | 214 | // Device functions 215 | window.WAPI.getHost = getHost; 216 | window.WAPI.getMe = getMe; 217 | window.WAPI.isLoggedIn = isLoggedIn; 218 | window.WAPI.isConnected = isConnected; 219 | window.WAPI.getBatteryLevel = getBatteryLevel; 220 | window.WAPI.base64ImageToFile = base64ToFile; 221 | window.WAPI.base64ToFile = base64ToFile; 222 | 223 | // Listeners initialization 224 | window.WAPI._newMessagesQueue = []; 225 | window.WAPI._newMessagesBuffer = 226 | sessionStorage.getItem('saved_msgs') != null 227 | ? JSON.parse(sessionStorage.getItem('saved_msgs')) 228 | : []; 229 | window.WAPI._newMessagesDebouncer = null; 230 | window.WAPI._newMessagesCallbacks = []; 231 | window.Store.Msg.off('add'); 232 | sessionStorage.removeItem('saved_msgs'); 233 | initNewMessagesListener(); 234 | 235 | // Listeners 236 | window.addEventListener('unload', window.WAPI._unloadInform, false); 237 | window.addEventListener('beforeunload', window.WAPI._unloadInform, false); 238 | window.addEventListener('pageunload', window.WAPI._unloadInform, false); 239 | addNewMessagesListener(); 240 | allNewMessagesListener(); 241 | addOnStateChange(); 242 | addOnNewAcks(); 243 | addOnLiveLocation(); 244 | addOnParticipantsChange(); 245 | addOnAddedToGroup(); 246 | 247 | // On-work below: 248 | 249 | /** 250 | * New version of @tag message 251 | */ 252 | window.WAPI.sendMessageMentioned = async function (chatId, message, mentioned) { 253 | if (!Array.isArray(mentioned)) { 254 | mentioned = [mentioned]; 255 | } 256 | 257 | const chat = WAPI.getChat(chatId); 258 | const users = await Store.Contact.serialize().filter((x) => 259 | mentioned.includes(x.id.user) 260 | ); 261 | 262 | chat.sendMessage(message, { 263 | linkPreview: null, 264 | mentionedJidList: users.map((u) => u.id), 265 | quotedMsg: null, 266 | quotedMsgAdminGroupJid: null, 267 | }); 268 | }; 269 | 270 | window.WAPI.getProfilePicSmallFromId = function (id, done) { 271 | window.Store.ProfilePicThumb.find(id).then( 272 | function (d) { 273 | if (d.img !== undefined) { 274 | window.WAPI.downloadFileWithCredentials(d.img, done); 275 | } else { 276 | done(false); 277 | } 278 | }, 279 | function (e) { 280 | done(false); 281 | } 282 | ); 283 | }; 284 | 285 | window.WAPI.getProfilePicFromId = function (id, done) { 286 | window.Store.ProfilePicThumb.find(id).then( 287 | function (d) { 288 | if (d.imgFull !== undefined) { 289 | window.WAPI.downloadFileWithCredentials(d.imgFull, done); 290 | } else { 291 | done(false); 292 | } 293 | }, 294 | function (e) { 295 | done(false); 296 | } 297 | ); 298 | }; 299 | 300 | window.WAPI.getWAVersion = function () { 301 | return window.Debug.VERSION; 302 | }; 303 | -------------------------------------------------------------------------------- /src/lib/wapi/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './wapi.js', 5 | mode: 'development', 6 | devtool: 'source-map', 7 | output: { 8 | path: path.resolve(__dirname, '../../../dist/lib/wapi'), 9 | filename: 'wapi.js', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/utils/semver.ts: -------------------------------------------------------------------------------- 1 | const VPAT = /^\d+(\.\d+){0,2}$/; 2 | 3 | /** 4 | * Compares two versions 5 | * @return true if local is up to date, false otherwise 6 | * @param local 7 | * @param remote 8 | */ 9 | export function upToDate(local: string, remote: string) { 10 | if (!local || !remote || local.length === 0 || remote.length === 0) 11 | return false; 12 | if (local == remote) return true; 13 | if (VPAT.test(local) && VPAT.test(remote)) { 14 | const lparts = local.split('.'); 15 | while (lparts.length < 3) lparts.push('0'); 16 | const rparts = remote.split('.'); 17 | while (rparts.length < 3) rparts.push('0'); 18 | for (let i = 0; i < 3; i++) { 19 | const l = parseInt(lparts[i], 10); 20 | const r = parseInt(rparts[i], 10); 21 | if (l === r) continue; 22 | return l > r; 23 | } 24 | return true; 25 | } else { 26 | return local >= remote; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Sleep async function 3 | * @param time 4 | */ 5 | export function sleep(time: number): Promise { 6 | return new Promise((resolve) => setTimeout(resolve, time)); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/wait-for-response.ts: -------------------------------------------------------------------------------- 1 | import { Page, Response } from 'puppeteer'; 2 | 3 | export function waitForResponse( 4 | page: Page, 5 | url: string, 6 | withFn?: (response: Response) => boolean | Promise 7 | ): Promise<{ response: T; statusCode: number }> { 8 | return new Promise((resolve, reject) => { 9 | page.on('response', async function callback(response) { 10 | if (response.url().includes(url)) { 11 | // Generate result 12 | const genResult = async () => { 13 | const json = await response 14 | .json() 15 | .then((r: T) => r as T) 16 | .catch(() => null); 17 | 18 | if (json) { 19 | resolve({ response: json, statusCode: response.status() }); 20 | } 21 | page.removeListener('response', callback); 22 | }; 23 | 24 | if (withFn !== undefined) { 25 | if (await withFn(response)) { 26 | genResult(); 27 | } 28 | } else { 29 | genResult(); 30 | } 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | export const jsonable = async (response: Response) => { 37 | try { 38 | await response.json(); 39 | return true; 40 | } catch (err) { 41 | return false; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "resolveJsonModule": true, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | "removeComments": false /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | // "strict": true, /* Enable all strict type-checking options. */ 26 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 47 | // "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | 56 | /* Experimental Options */ 57 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 58 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 59 | }, 60 | "include": ["src/**/*"], 61 | "exclude": ["src/middleware/middleware.ts", "img/*"] 62 | } 63 | --------------------------------------------------------------------------------