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