Integrated whatsapp-web from npm

This commit is contained in:
Mohammed Ashab Uddin
2020-07-19 09:56:55 +04:00
parent cba2828f38
commit 64a4b81f1d
79 changed files with 29 additions and 16511 deletions

View File

@@ -1,663 +0,0 @@
'use strict';
const EventEmitter = require('events');
const puppeteer = require('puppeteer');
const moduleRaid = require('@pedroslopez/moduleraid/moduleraid');
const jsQR = require('jsqr');
const Util = require('./util/Util');
const InterfaceController = require('./util/InterfaceController');
const { WhatsWebURL, DefaultOptions, Events, WAState } = require('./util/Constants');
const { ExposeStore, LoadUtils } = require('./util/Injected');
const ChatFactory = require('./factories/ChatFactory');
const ContactFactory = require('./factories/ContactFactory');
const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification } = require('./structures');
/**
* Starting point for interacting with the WhatsApp Web API
* @extends {EventEmitter}
* @param {object} options
* @fires Client#qr
* @fires Client#authenticated
* @fires Client#auth_failure
* @fires Client#ready
* @fires Client#message
* @fires Client#message_ack
* @fires Client#message_create
* @fires Client#message_revoke_me
* @fires Client#message_revoke_everyone
* @fires Client#media_uploaded
* @fires Client#group_join
* @fires Client#group_leave
* @fires Client#group_update
* @fires Client#disconnected
* @fires Client#change_state
* @fires Client#change_battery
*/
class Client extends EventEmitter {
constructor(options = {}) {
super();
this.options = Util.mergeDefault(DefaultOptions, options);
this.pupBrowser = null;
this.pupPage = null;
}
/**
* Sets up events and requirements, kicks off authentication request
*/
async initialize() {
const browser = await puppeteer.launch(this.options.puppeteer);
const page = (await browser.pages())[0];
page.setUserAgent(this.options.userAgent);
this.pupBrowser = browser;
this.pupPage = page;
if (this.options.session) {
await page.evaluateOnNewDocument(
session => {
localStorage.clear();
localStorage.setItem('WABrowserId', session.WABrowserId);
localStorage.setItem('WASecretBundle', session.WASecretBundle);
localStorage.setItem('WAToken1', session.WAToken1);
localStorage.setItem('WAToken2', session.WAToken2);
}, this.options.session);
}
await page.goto(WhatsWebURL, {
waitUntil: 'load',
timeout: 0,
});
const KEEP_PHONE_CONNECTED_IMG_SELECTOR = '[data-asset-intro-image-light="true"]';
if (this.options.session) {
// Check if session restore was successfull
try {
await page.waitForSelector(KEEP_PHONE_CONNECTED_IMG_SELECTOR, { timeout: this.options.authTimeoutMs });
} catch (err) {
if (err.name === 'TimeoutError') {
/**
* Emitted when there has been an error while trying to restore an existing session
* @event Client#auth_failure
* @param {string} message
*/
this.emit(Events.AUTHENTICATION_FAILURE, 'Unable to log in. Are the session details valid?');
browser.close();
if (this.options.restartOnAuthFail) {
// session restore failed so try again but without session to force new authentication
this.options.session = null;
this.initialize();
}
return;
}
throw err;
}
} else {
const getQrCode = async () => {
// Check if retry button is present
var QR_RETRY_SELECTOR = 'div[data-ref] > span > div';
var qrRetry = await page.$(QR_RETRY_SELECTOR);
if (qrRetry) {
await qrRetry.click();
}
// Wait for QR Code
const QR_CANVAS_SELECTOR = 'canvas';
await page.waitForSelector(QR_CANVAS_SELECTOR, { timeout: this.options.qrTimeoutMs });
const qrImgData = await page.$eval(QR_CANVAS_SELECTOR, canvas => [].slice.call(canvas.getContext('2d').getImageData(0, 0, 264, 264).data));
const qr = jsQR(qrImgData, 264, 264).data;
/**
* Emitted when the QR code is received
* @event Client#qr
* @param {string} qr QR Code
*/
this.emit(Events.QR_RECEIVED, qr);
};
getQrCode();
this._qrRefreshInterval = setInterval(getQrCode, this.options.qrRefreshIntervalMs);
// Wait for code scan
await page.waitForSelector(KEEP_PHONE_CONNECTED_IMG_SELECTOR, { timeout: 0 });
clearInterval(this._qrRefreshInterval);
this._qrRefreshInterval = undefined;
}
await page.evaluate(ExposeStore, moduleRaid.toString());
// Get session tokens
const localStorage = JSON.parse(await page.evaluate(() => {
return JSON.stringify(window.localStorage);
}));
const session = {
WABrowserId: localStorage.WABrowserId,
WASecretBundle: localStorage.WASecretBundle,
WAToken1: localStorage.WAToken1,
WAToken2: localStorage.WAToken2
};
/**
* Emitted when authentication is successful
* @event Client#authenticated
* @param {object} session Object containing session information. Can be used to restore the session.
*/
this.emit(Events.AUTHENTICATED, session);
// Check window.Store Injection
await page.waitForFunction('window.Store != undefined');
//Load util functions (serializers, helper functions)
await page.evaluate(LoadUtils);
// Expose client info
this.info = new ClientInfo(this, await page.evaluate(() => {
return window.Store.Conn.serialize();
}));
// Add InterfaceController
this.interface = new InterfaceController(this);
// Register events
await page.exposeFunction('onAddMessageEvent', msg => {
if (!msg.isNewMsg) return;
if (msg.type === 'gp2') {
const notification = new GroupNotification(this, msg);
if (msg.subtype === 'add' || msg.subtype === 'invite') {
/**
* Emitted when a user joins the chat via invite link or is added by an admin.
* @event Client#group_join
* @param {GroupNotification} notification GroupNotification with more information about the action
*/
this.emit(Events.GROUP_JOIN, notification);
} else if (msg.subtype === 'remove' || msg.subtype === 'leave') {
/**
* Emitted when a user leaves the chat or is removed by an admin.
* @event Client#group_leave
* @param {GroupNotification} notification GroupNotification with more information about the action
*/
this.emit(Events.GROUP_LEAVE, notification);
} else {
/**
* Emitted when group settings are updated, such as subject, description or picture.
* @event Client#group_update
* @param {GroupNotification} notification GroupNotification with more information about the action
*/
this.emit(Events.GROUP_UPDATE, notification);
}
return;
}
const message = new Message(this, msg);
/**
* Emitted when a new message is created, which may include the current user's own messages.
* @event Client#message_create
* @param {Message} message The message that was created
*/
this.emit(Events.MESSAGE_CREATE, message);
if (msg.id.fromMe) return;
/**
* Emitted when a new message is received.
* @event Client#message
* @param {Message} message The message that was received
*/
this.emit(Events.MESSAGE_RECEIVED, message);
});
let last_message;
await page.exposeFunction('onChangeMessageTypeEvent', (msg) => {
if (msg.type === 'revoked') {
const message = new Message(this, msg);
let revoked_msg;
if (last_message && msg.id.id === last_message.id.id) {
revoked_msg = new Message(this, last_message);
}
/**
* Emitted when a message is deleted for everyone in the chat.
* @event Client#message_revoke_everyone
* @param {Message} message The message that was revoked, in its current state. It will not contain the original message's data.
* @param {?Message} revoked_msg The message that was revoked, before it was revoked. It will contain the message's original data.
* Note that due to the way this data is captured, it may be possible that this param will be undefined.
*/
this.emit(Events.MESSAGE_REVOKED_EVERYONE, message, revoked_msg);
}
});
await page.exposeFunction('onChangeMessageEvent', (msg) => {
if (msg.type !== 'revoked') {
last_message = msg;
}
});
await page.exposeFunction('onRemoveMessageEvent', (msg) => {
if (!msg.isNewMsg) return;
const message = new Message(this, msg);
/**
* Emitted when a message is deleted by the current user.
* @event Client#message_revoke_me
* @param {Message} message The message that was revoked
*/
this.emit(Events.MESSAGE_REVOKED_ME, message);
});
await page.exposeFunction('onMessageAckEvent', (msg, ack) => {
const message = new Message(this, msg);
/**
* Emitted when an ack event occurrs on message type.
* @event Client#message_ack
* @param {Message} message The message that was affected
* @param {MessageAck} ack The new ACK value
*/
this.emit(Events.MESSAGE_ACK, message, ack);
});
await page.exposeFunction('onMessageMediaUploadedEvent', (msg) => {
const message = new Message(this, msg);
/**
* Emitted when media has been uploaded for a message sent by the client.
* @event Client#media_uploaded
* @param {Message} message The message with media that was uploaded
*/
this.emit(Events.MEDIA_UPLOADED, message);
});
await page.exposeFunction('onAppStateChangedEvent', (state) => {
/**
* Emitted when the connection state changes
* @event Client#change_state
* @param {WAState} state the new connection state
*/
this.emit(Events.STATE_CHANGED, state);
const ACCEPTED_STATES = [WAState.CONNECTED, WAState.OPENING, WAState.PAIRING, WAState.TIMEOUT];
if(this.options.takeoverOnConflict) {
ACCEPTED_STATES.push(WAState.CONFLICT);
if(state === WAState.CONFLICT) {
setTimeout(() => {
this.pupPage.evaluate(() => window.Store.AppState.takeover());
}, this.options.takeoverTimeoutMs);
}
}
if (!ACCEPTED_STATES.includes(state)) {
/**
* Emitted when the client has been disconnected
* @event Client#disconnected
* @param {WAState} reason state that caused the disconnect
*/
this.emit(Events.DISCONNECTED, state);
this.destroy();
}
});
await page.exposeFunction('onBatteryStateChangedEvent', (state) => {
const { battery, plugged } = state;
if(battery === undefined) return;
/**
* Emitted when the battery percentage for the attached device changes
* @event Client#change_battery
* @param {object} batteryInfo
* @param {number} batteryInfo.battery - The current battery percentage
* @param {boolean} batteryInfo.plugged - Indicates if the phone is plugged in (true) or not (false)
*/
this.emit(Events.BATTERY_CHANGED, { battery, plugged });
});
await page.evaluate(() => {
window.Store.Msg.on('add', (msg) => { if(msg.isNewMsg) window.onAddMessageEvent(msg); });
window.Store.Msg.on('change', (msg) => { window.onChangeMessageEvent(msg); });
window.Store.Msg.on('change:type', (msg) => { window.onChangeMessageTypeEvent(msg); });
window.Store.Msg.on('change:ack', (msg, ack) => { window.onMessageAckEvent(msg, ack); });
window.Store.Msg.on('change:isUnsentMedia', (msg, unsent) => { if(msg.id.fromMe && !unsent) window.onMessageMediaUploadedEvent(msg); });
window.Store.Msg.on('remove', (msg) => { if(msg.isNewMsg) window.onRemoveMessageEvent(msg); });
window.Store.AppState.on('change:state', (_AppState, state) => { window.onAppStateChangedEvent(state); });
window.Store.Conn.on('change:battery', (state) => { window.onBatteryStateChangedEvent(state); });
});
/**
* Emitted when the client has initialized and is ready to receive messages.
* @event Client#ready
*/
this.emit(Events.READY);
}
/**
* Closes the client
*/
async destroy() {
if (this._qrRefreshInterval) {
clearInterval(this._qrRefreshInterval);
}
await this.pupBrowser.close();
}
/**
* Logs out the client, closing the current session
*/
async logout() {
return await this.pupPage.evaluate(() => {
return window.Store.AppState.logout();
});
}
/**
* Returns the version of WhatsApp Web currently being run
* @returns {Promise<string>}
*/
async getWWebVersion() {
return await this.pupPage.evaluate(() => {
return window.Debug.VERSION;
});
}
/**
* Mark as seen for the Chat
* @param {string} chatId
* @returns {Promise<boolean>} result
*
*/
async sendSeen(chatId) {
const result = await this.pupPage.evaluate(async (chatId) => {
return window.WWebJS.sendSeen(chatId);
}, chatId);
return result;
}
/**
* Send a message to a specific chatId
* @param {string} chatId
* @param {string|MessageMedia|Location} content
* @param {object} options
* @returns {Promise<Message>} Message that was just sent
*/
async sendMessage(chatId, content, options = {}) {
let internalOptions = {
linkPreview: options.linkPreview === false ? undefined : true,
sendAudioAsVoice: options.sendAudioAsVoice,
caption: options.caption,
quotedMessageId: options.quotedMessageId,
mentionedJidList: Array.isArray(options.mentions) ? options.mentions.map(contact => contact.id._serialized) : []
};
const sendSeen = typeof options.sendSeen === 'undefined' ? true : options.sendSeen;
if (content instanceof MessageMedia) {
internalOptions.attachment = content;
content = '';
} else if (options.media instanceof MessageMedia) {
internalOptions.attachment = options.media;
internalOptions.caption = content;
content = '';
} else if (content instanceof Location) {
internalOptions.location = content;
content = '';
}
const newMessage = await this.pupPage.evaluate(async (chatId, message, options, sendSeen) => {
const chatWid = window.Store.WidFactory.createWid(chatId);
const chat = await window.Store.Chat.find(chatWid);
if(sendSeen) {
window.WWebJS.sendSeen(chatId);
}
const msg = await window.WWebJS.sendMessage(chat, message, options, sendSeen);
return msg.serialize();
}, chatId, content, internalOptions, sendSeen);
return new Message(this, newMessage);
}
/**
* Get all current chat instances
* @returns {Promise<Array<Chat>>}
*/
async getChats() {
let chats = await this.pupPage.evaluate(() => {
return window.WWebJS.getChats();
});
return chats.map(chat => ChatFactory.create(this, chat));
}
/**
* Get chat instance by ID
* @param {string} chatId
* @returns {Promise<Chat>}
*/
async getChatById(chatId) {
let chat = await this.pupPage.evaluate(chatId => {
return window.WWebJS.getChat(chatId);
}, chatId);
return ChatFactory.create(this, chat);
}
/**
* Get all current contact instances
* @returns {Promise<Array<Contact>>}
*/
async getContacts() {
let contacts = await this.pupPage.evaluate(() => {
return window.WWebJS.getContacts();
});
return contacts.map(contact => ContactFactory.create(this, contact));
}
/**
* Get contact instance by ID
* @param {string} contactId
* @returns {Promise<Contact>}
*/
async getContactById(contactId) {
let contact = await this.pupPage.evaluate(contactId => {
return window.WWebJS.getContact(contactId);
}, contactId);
return ContactFactory.create(this, contact);
}
/**
* Returns an object with information about the invite code's group
* @param {string} inviteCode
* @returns {Promise<object>} Invite information
*/
async getInviteInfo(inviteCode) {
return await this.pupPage.evaluate(inviteCode => {
return window.Store.Wap.groupInviteInfo(inviteCode);
}, inviteCode);
}
/**
* Accepts an invitation to join a group
* @param {string} inviteCode Invitation code
*/
async acceptInvite(inviteCode) {
const chatId = await this.pupPage.evaluate(async inviteCode => {
return await window.Store.Invite.sendJoinGroupViaInvite(inviteCode);
}, inviteCode);
return chatId._serialized;
}
/**
* Sets the current user's status message
* @param {string} status New status message
*/
async setStatus(status) {
await this.pupPage.evaluate(async status => {
return await window.Store.Wap.sendSetStatus(status);
}, status);
}
/**
* Gets the current connection state for the client
* @returns {WAState}
*/
async getState() {
return await this.pupPage.evaluate(() => {
return window.Store.AppState.state;
});
}
/**
* Marks the client as online
*/
async sendPresenceAvailable() {
return await this.pupPage.evaluate(() => {
return window.Store.Wap.sendPresenceAvailable();
});
}
/**
* Enables and returns the archive state of the Chat
* @returns {boolean}
*/
async archiveChat(chatId) {
return await this.pupPage.evaluate(async chatId => {
let chat = await window.Store.Chat.get(chatId);
await window.Store.Cmd.archiveChat(chat, true);
return chat.archive;
}, chatId);
}
/**
* Changes and returns the archive state of the Chat
* @returns {boolean}
*/
async unarchiveChat(chatId) {
return await this.pupPage.evaluate(async chatId => {
let chat = await window.Store.Chat.get(chatId);
await window.Store.Cmd.archiveChat(chat, false);
return chat.archive;
}, chatId);
}
/**
* Mutes the Chat until a specified date
* @param {string} chatId ID of the chat that will be muted
* @param {Date} unmuteDate Date when the chat will be unmuted
*/
async muteChat(chatId, unmuteDate) {
await this.pupPage.evaluate(async (chatId, timestamp) => {
let chat = await window.Store.Chat.get(chatId);
await chat.mute.mute(timestamp, !0);
}, chatId, unmuteDate.getTime() / 1000);
}
/**
* Unmutes the Chat
* @param {string} chatId ID of the chat that will be unmuted
*/
async unmuteChat(chatId) {
await this.pupPage.evaluate(async chatId => {
let chat = await window.Store.Chat.get(chatId);
await window.Store.Cmd.muteChat(chat, false);
}, chatId);
}
/**
* Returns the contact ID's profile picture URL, if privacy settings allow it
* @param {string} contactId the whatsapp user's ID
* @returns {Promise<string>}
*/
async getProfilePicUrl(contactId) {
const profilePic = await this.pupPage.evaluate((contactId) => {
return window.Store.Wap.profilePicFind(contactId);
}, contactId);
return profilePic ? profilePic.eurl : undefined;
}
/**
* Force reset of connection state for the client
*/
async resetState(){
await this.pupPage.evaluate(() => {
window.Store.AppState.phoneWatchdog.shiftTimer.forceRunNow();
});
}
/**
* Check if a given ID is registered in whatsapp
* @param {string} id the whatsapp user's ID
* @returns {Promise<Boolean>}
*/
async isRegisteredUser(id) {
return await this.pupPage.evaluate(async (id) => {
let result = await window.Store.Wap.queryExist(id);
return result.jid !== undefined;
}, id);
}
/**
* Create a new group
* @param {string} name group title
* @param {Array<Contact|string>} participants an array of Contacts or contact IDs to add to the group
* @returns {Object} createRes
* @returns {string} createRes.gid - ID for the group that was just created
* @returns {Object.<string,string>} createRes.missingParticipants - participants that were not added to the group. Keys represent the ID for participant that was not added and its value is a status code that represents the reason why participant could not be added. This is usually 403 if the user's privacy settings don't allow you to add them to groups.
*/
async createGroup(name, participants) {
if(!Array.isArray(participants) || participants.length == 0) {
throw 'You need to add at least one other participant to the group';
}
if(participants.every(c => c instanceof Contact)) {
participants = participants.map(c => c.id._serialized);
}
const createRes = await this.pupPage.evaluate(async (name, participantIds) => {
const res = await window.Store.Wap.createGroup(name, participantIds);
console.log(res);
if(!res.status === 200) {
throw 'An error occurred while creating the group!';
}
return res;
}, name, participants);
const missingParticipants = createRes.participants.reduce(((missing, c) => {
const id = Object.keys(c)[0];
const statusCode = c[id].code;
if(statusCode != 200) return Object.assign(missing, {[id]: statusCode});
return missing;
}), {});
return { gid: createRes.gid, missingParticipants};
}
}
module.exports = Client;

View File

@@ -1,16 +0,0 @@
'use strict';
const PrivateChat = require('../structures/PrivateChat');
const GroupChat = require('../structures/GroupChat');
class ChatFactory {
static create(client, data) {
if(data.isGroup) {
return new GroupChat(client, data);
}
return new PrivateChat(client, data);
}
}
module.exports = ChatFactory;

View File

@@ -1,16 +0,0 @@
'use strict';
const PrivateContact = require('../structures/PrivateContact');
const BusinessContact = require('../structures/BusinessContact');
class ContactFactory {
static create(client, data) {
if(data.isBusiness) {
return new BusinessContact(client, data);
}
return new PrivateContact(client, data);
}
}
module.exports = ContactFactory;

View File

@@ -1,22 +0,0 @@
'use strict';
/**
* Represents a WhatsApp data structure
*/
class Base {
constructor(client) {
/**
* The client that instantiated this
* @readonly
*/
Object.defineProperty(this, 'client', { value: client });
}
_clone() {
return Object.assign(Object.create(this), this);
}
_patch(data) { return data; }
}
module.exports = Base;

View File

@@ -1,21 +0,0 @@
'use strict';
const Contact = require('./Contact');
/**
* Represents a Business Contact on WhatsApp
* @extends {Contact}
*/
class BusinessContact extends Contact {
_patch(data) {
/**
* The contact's business profile
*/
this.businessProfile = data.businessProfile;
return super._patch(data);
}
}
module.exports = BusinessContact;

View File

@@ -1,191 +0,0 @@
'use strict';
const Base = require('./Base');
const Message = require('./Message');
/**
* Represents a Chat on WhatsApp
* @extends {Base}
*/
class Chat extends Base {
constructor(client, data) {
super(client);
if (data) this._patch(data);
}
_patch(data) {
/**
* ID that represents the chat
* @type {object}
*/
this.id = data.id;
/**
* Title of the chat
* @type {string}
*/
this.name = data.formattedTitle;
/**
* Indicates if the Chat is a Group Chat
* @type {boolean}
*/
this.isGroup = data.isGroup;
/**
* Indicates if the Chat is readonly
* @type {boolean}
*/
this.isReadOnly = data.isReadOnly;
/**
* Amount of messages unread
* @type {number}
*/
this.unreadCount = data.unreadCount;
/**
* Unix timestamp for when the chat was created
* @type {number}
*/
this.timestamp = data.t;
/**
* Indicates if the Chat is archived
* @type {boolean}
*/
this.archived = data.archive;
return super._patch(data);
}
/**
* Send a message to this chat
* @param {string|MessageMedia|Location} content
* @param {object} options
* @returns {Promise<Message>} Message that was just sent
*/
async sendMessage(content, options) {
return this.client.sendMessage(this.id._serialized, content, options);
}
/**
* Set the message as seen
* @returns {Promise<Boolean>} result
*/
async sendSeen() {
return this.client.sendSeen(this.id._serialized);
}
/**
* Clears all messages from the chat
* @returns {Promise<Boolean>} result
*/
async clearMessages() {
return this.client.pupPage.evaluate(chatId => {
return window.WWebJS.sendClearChat(chatId);
}, this.id._serialized);
}
/**
* Deletes the chat
* @returns {Promise<Boolean>} result
*/
async delete() {
return this.client.pupPage.evaluate(chatId => {
return window.WWebJS.sendDeleteChat(chatId);
}, this.id._serialized);
}
/**
* Archives this chat
*/
async archive() {
return this.client.archiveChat(this.id._serialized);
}
/**
* un-archives this chat
*/
async unarchive() {
return this.client.unarchiveChat(this.id._serialized);
}
/**
* Mutes this chat until a specified date
* @param {Date} unmuteDate Date at which the Chat will be unmuted
*/
async mute(unmuteDate) {
return this.client.muteChat(this.id._serialized, unmuteDate);
}
/**
* Unmutes this chat
*/
async unmute() {
return this.client.unmuteChat(this.id._serialized);
}
/**
* Loads chat messages, sorted from earliest to latest.
* @param {Object} searchOptions Options for searching messages. Right now only limit is supported.
* @param {Number} [searchOptions.limit=50] The amount of messages to return. Note that the actual number of returned messages may be smaller if there aren't enough messages in the conversation. Set this to Infinity to load all messages.
* @returns {Promise<Array<Message>>}
*/
async fetchMessages(searchOptions) {
if(!searchOptions || !searchOptions.limit) {
searchOptions = {limit: 50};
}
let messages = await this.client.pupPage.evaluate(async (chatId, limit) => {
const msgFilter = m => !m.isNotification; // dont include notification messages
const chat = window.Store.Chat.get(chatId);
let msgs = chat.msgs.models.filter(msgFilter);
while(msgs.length < limit) {
const loadedMessages = await chat.loadEarlierMsgs();
if(!loadedMessages) break;
msgs = [...loadedMessages.filter(msgFilter), ...msgs];
}
msgs.sort((a, b) => (a.t > b.t) ? 1 : -1);
return msgs.splice(msgs.length - limit).map(m => m.serialize());
}, this.id._serialized, searchOptions.limit);
return messages.map(m => new Message(this.client, m));
}
/**
* Simulate typing in chat. This will last for 25 seconds.
*/
async sendStateTyping() {
return this.client.pupPage.evaluate(chatId => {
window.WWebJS.sendChatstate('typing', chatId);
return true;
}, this.id._serialized);
}
/**
* Simulate recording audio in chat. This will last for 25 seconds.
*/
async sendStateRecording() {
return this.client.pupPage.evaluate(chatId => {
window.WWebJS.sendChatstate('recording', chatId);
return true;
}, this.id._serialized);
}
/**
* Stops typing or recording in chat immediately.
*/
async clearState() {
return this.client.pupPage.evaluate(chatId => {
window.WWebJS.sendChatstate('stop', chatId);
return true;
}, this.id._serialized);
}
}
module.exports = Chat;

View File

@@ -1,64 +0,0 @@
'use strict';
const Base = require('./Base');
/**
* Current connection information
* @extends {Base}
*/
class ClientInfo extends Base {
constructor(client, data) {
super(client);
if(data) this._patch(data);
}
_patch(data) {
/**
* Name configured to be shown in push notifications
* @type {string}
*/
this.pushname = data.pushname;
/**
* Current user ID
* @type {object}
*/
this.me = data.me;
/**
* Information about the phone this client is connected to
* @type {object}
* @property {string} wa_version WhatsApp Version running on the phone
* @property {string} os_version OS Version running on the phone (iOS or Android version)
* @property {string} device_manufacturer Device manufacturer
* @property {string} device_model Device model
* @property {string} os_build_number OS build number
*/
this.phone = data.phone;
/**
* Platform the phone is running on
* @type {string}
*/
this.platform = data.platform;
return super._patch(data);
}
/**
* Get current battery percentage and charging status for the attached device
* @returns {object} batteryStatus
* @returns {number} batteryStatus.battery - The current battery percentage
* @returns {boolean} batteryStatus.plugged - Indicates if the phone is plugged in (true) or not (false)
*/
async getBatteryStatus() {
return await this.client.pupPage.evaluate(() => {
const { battery, plugged } = window.Store.Conn;
return { battery, plugged };
});
}
}
module.exports = ClientInfo;

View File

@@ -1,111 +0,0 @@
'use strict';
const Base = require('./Base');
/**
* Represents a Contact on WhatsApp
* @extends {Base}
*/
class Contact extends Base {
constructor(client, data) {
super(client);
if(data) this._patch(data);
}
_patch(data) {
/**
* ID that represents the contact
* @type {object}
*/
this.id = data.id;
/**
* Contact's phone number
* @type {string}
*/
this.number = data.userid;
/**
* Indicates if the contact is a business contact
* @type {boolean}
*/
this.isBusiness = data.isBusiness;
/**
* Indicates if the contact is an enterprise contact
* @type {boolean}
*/
this.isEnterprise = data.isEnterprise;
this.labels = data.labels;
/**
* The contact's name, as saved by the current user
* @type {?string}
*/
this.name = data.name;
/**
* The name that the contact has configured to be shown publically
* @type {string}
*/
this.pushname = data.pushname;
this.sectionHeader = data.sectionHeader;
/**
* A shortened version of name
* @type {?string}
*/
this.shortName = data.shortName;
this.statusMute = data.statusMute;
this.type = data.type;
this.verifiedLevel = data.verifiedLevel;
this.verifiedName = data.verifiedName;
/**
* Indicates if the contact is the current user's contact
* @type {boolean}
*/
this.isMe = data.isMe;
/**
* Indicates if the contact is a user contact
* @type {boolean}
*/
this.isUser = data.isUser;
/**
* Indicates if the contact is a group contact
* @type {boolean}
*/
this.isGroup = data.isGroup;
/**
* Indicates if the number is registered on WhatsApp
* @type {boolean}
*/
this.isWAContact = data.isWAContact;
/**
* Indicates if the number is saved in the current phone's contacts
* @type {boolean}
*/
this.isMyContact = data.isMyContact;
return super._patch(data);
}
/**
* Returns the contact's profile picture URL, if privacy settings allow it
* @returns {Promise<string>}
*/
async getProfilePicUrl() {
return await this.client.getProfilePicUrl(this.id._serialized);
}
}
module.exports = Contact;

View File

@@ -1,150 +0,0 @@
'use strict';
const Chat = require('./Chat');
/**
* Represents a Group Chat on WhatsApp
* @extends {Chat}
*/
class GroupChat extends Chat {
_patch(data) {
this.groupMetadata = data.groupMetadata;
return super._patch(data);
}
/**
* Gets the group owner
*/
get owner() {
return this.groupMetadata.owner;
}
/**
* Gets the date at which the group was created
* @type {date}
*/
get createdAt() {
return new Date(this.groupMetadata.creation * 1000);
}
/**
* Gets the group description
* @type {string}
*/
get description() {
return this.groupMetadata.desc;
}
/**
* Gets the group participants
* @type {array}
*/
get participants() {
return this.groupMetadata.participants;
}
/**
* Adds a list of participants by ID to the group
* @param {Array<string>} participantIds
*/
async addParticipants(participantIds) {
return await this.client.pupPage.evaluate((chatId, participantIds) => {
return window.Store.Wap.addParticipants(chatId, participantIds);
}, this.id._serialized, participantIds);
}
/**
* Removes a list of participants by ID to the group
* @param {Array<string>} participantIds
*/
async removeParticipants(participantIds) {
return await this.client.pupPage.evaluate((chatId, participantIds) => {
return window.Store.Wap.removeParticipants(chatId, participantIds);
}, this.id._serialized, participantIds);
}
/**
* Promotes participants by IDs to admins
* @param {Array<string>} participantIds
*/
async promoteParticipants(participantIds) {
return await this.client.pupPage.evaluate((chatId, participantIds) => {
return window.Store.Wap.promoteParticipants(chatId, participantIds);
}, this.id._serialized, participantIds);
}
/**
* Demotes participants by IDs to regular users
* @param {Array<string>} participantIds
*/
async demoteParticipants(participantIds) {
return await this.client.pupPage.evaluate((chatId, participantIds) => {
return window.Store.Wap.demoteParticipants(chatId, participantIds);
}, this.id._serialized, participantIds);
}
/**
* Updates the group subject
* @param {string} subject
*/
async setSubject(subject) {
let res = await this.client.pupPage.evaluate((chatId, subject) => {
return window.Store.Wap.changeSubject(chatId, subject);
}, this.id._serialized, subject);
if(res.status == 200) {
this.name = subject;
}
}
/**
* Updates the group description
* @param {string} description
*/
async setDescription(description) {
let res = await this.client.pupPage.evaluate((chatId, description) => {
let descId = window.Store.GroupMetadata.get(chatId).descId;
return window.Store.Wap.setGroupDescription(chatId, description, window.Store.genId(), descId);
}, this.id._serialized, description);
if (res.status == 200) {
this.groupMetadata.desc = description;
}
}
/**
* Gets the invite code for a specific group
*/
async getInviteCode() {
let res = await this.client.pupPage.evaluate(chatId => {
return window.Store.Wap.groupInviteCode(chatId);
}, this.id._serialized);
if (res.status == 200) {
return res.code;
}
throw new Error('Not authorized');
}
/**
* Invalidates the current group invite code and generates a new one
*/
async revokeInvite() {
return await this.client.pupPage.evaluate(chatId => {
return window.Store.Wap.revokeGroupInvite(chatId);
}, this.id._serialized);
}
/**
* Makes the bot leave the group
*/
async leave() {
return await this.client.pupPage.evaluate(chatId => {
return window.Store.Wap.leaveGroup(chatId);
}, this.id._serialized);
}
}
module.exports = GroupChat;

View File

@@ -1,104 +0,0 @@
'use strict';
const Base = require('./Base');
/**
* Represents a GroupNotification on WhatsApp
* @extends {Base}
*/
class GroupNotification extends Base {
constructor(client, data) {
super(client);
if(data) this._patch(data);
}
_patch(data) {
/**
* ID that represents the groupNotification
* @type {object}
*/
this.id = data.id;
/**
* Extra content
* @type {string}
*/
this.body = data.body || '';
/**
* GroupNotification type
* @type {GroupNotificationTypes}
*/
this.type = data.subtype;
/**
* Unix timestamp for when the groupNotification was created
* @type {number}
*/
this.timestamp = data.t;
/**
* ID for the Chat that this groupNotification was sent for.
*
* @type {string}
*/
this.chatId = typeof (data.from) === 'object' ? data.from._serialized : data.from;
/**
* ContactId for the user that produced the GroupNotification.
* @type {string}
*/
this.author = typeof (data.author) === 'object' ? data.author._serialized : data.author;
/**
* Contact IDs for the users that were affected by this GroupNotification.
* @type {Array<string>}
*/
this.recipientIds = [];
if (data.recipients) {
this.recipientIds = data.recipients;
}
return super._patch(data);
}
/**
* Returns the Chat this groupNotification was sent in
* @returns {Promise<Chat>}
*/
getChat() {
return this.client.getChatById(this.chatId);
}
/**
* Returns the Contact this GroupNotification was produced by
* @returns {Promise<Contact>}
*/
getContact() {
return this.client.getContactById(this.author);
}
/**
* Returns the Contacts affected by this GroupNotification.
* @returns {Promise<Array<Contact>>}
*/
async getRecipients() {
return await Promise.all(this.recipientIds.map(async m => await this.client.getContactById(m)));
}
/**
* Sends a message to the same chat this GroupNotification was produced in.
*
* @param {string|MessageMedia|Location} content
* @param {object} options
* @returns {Promise<Message>}
*/
async reply(content, options={}) {
return this.client.sendMessage(this.chatId, content, options);
}
}
module.exports = GroupNotification;

View File

@@ -1,33 +0,0 @@
'use strict';
/**
* Location information
*/
class Location {
/**
* @param {number} latitude
* @param {number} longitude
* @param {?string} description
*/
constructor(latitude, longitude, description) {
/**
* Location latitude
* @type {number}
*/
this.latitude = latitude;
/**
* Location longitude
* @type {number}
*/
this.longitude = longitude;
/**
* Name for the location
* @type {?string}
*/
this.description = description;
}
}
module.exports = Location;

View File

@@ -1,248 +0,0 @@
'use strict';
const Base = require('./Base');
const MessageMedia = require('./MessageMedia');
const Location = require('./Location');
const { MessageTypes } = require('../util/Constants');
/**
* Represents a Message on WhatsApp
* @extends {Base}
*/
class Message extends Base {
constructor(client, data) {
super(client);
if(data) this._patch(data);
}
_patch(data) {
/**
* MediaKey that represents the sticker 'ID'
* @type {string}
*/
this.mediaKey = data.mediaKey;
/**
* ID that represents the message
* @type {object}
*/
this.id = data.id;
/**
* ACK status for the message
* @type {MessageAck}
*/
this.ack = data.ack;
/**
* Indicates if the message has media available for download
* @type {boolean}
*/
this.hasMedia = data.clientUrl ? true : false;
/**
* Message content
* @type {string}
*/
this.body = this.hasMedia ? data.caption || '' : data.body || '';
/**
* Message type
* @type {MessageTypes}
*/
this.type = data.type;
/**
* Unix timestamp for when the message was created
* @type {number}
*/
this.timestamp = data.t;
/**
* ID for the Chat that this message was sent to, except if the message was sent by the current user.
* @type {string}
*/
this.from = (typeof (data.from) === 'object' && data.from !== null) ? data.from._serialized : data.from;
/**
* ID for who this message is for.
*
* If the message is sent by the current user, it will be the Chat to which the message is being sent.
* If the message is sent by another user, it will be the ID for the current user.
* @type {string}
*/
this.to = (typeof (data.to) === 'object' && data.to !== null) ? data.to._serialized : data.to;
/**
* If the message was sent to a group, this field will contain the user that sent the message.
* @type {string}
*/
this.author = (typeof (data.author) === 'object' && data.author !== null) ? data.author._serialized : data.author;
/**
* Indicates if the message was forwarded
* @type {boolean}
*/
this.isForwarded = data.isForwarded;
/**
* Indicates if the message was a broadcast
* @type {boolean}
*/
this.broadcast = data.broadcast;
/**
* Indicates if the message was sent by the current user
* @type {boolean}
*/
this.fromMe = data.id.fromMe;
/**
* Indicates if the message was sent as a reply to another message.
* @type {boolean}
*/
this.hasQuotedMsg = data.quotedMsg ? true : false;
/**
* Location information contained in the message, if the message is type "location"
* @type {Location}
*/
this.location = data.type === MessageTypes.LOCATION ? new Location(data.lat, data.lng, data.loc) : undefined;
/**
* Indicates the mentions in the message body.
* @type {Array<string>}
*/
this.mentionedIds = [];
if (data.mentionedJidList) {
this.mentionedIds = data.mentionedJidList;
}
return super._patch(data);
}
_getChatId() {
return this.fromMe ? this.to : this.from;
}
/**
* Returns the Chat this message was sent in
* @returns {Promise<Chat>}
*/
getChat() {
return this.client.getChatById(this._getChatId());
}
/**
* Returns the Contact this message was sent from
* @returns {Promise<Contact>}
*/
getContact() {
return this.client.getContactById(this.author || this.from);
}
/**
* Returns the Contacts mentioned in this message
* @returns {Promise<Array<Contact>>}
*/
async getMentions() {
return await Promise.all(this.mentionedIds.map(async m => await this.client.getContactById(m)));
}
/**
* Returns the quoted message, if any
* @returns {Promise<Message>}
*/
async getQuotedMessage() {
if (!this.hasQuotedMsg) return undefined;
const quotedMsg = await this.client.pupPage.evaluate((msgId) => {
let msg = window.Store.Msg.get(msgId);
return msg.quotedMsgObj().serialize();
}, this.id._serialized);
return new Message(this.client, quotedMsg);
}
/**
* Sends a message as a reply to this message. If chatId is specified, it will be sent
* through the specified Chat. If not, it will send the message
* in the same Chat as the original message was sent.
*
* @param {string|MessageMedia|Location} content
* @param {?string} chatId
* @param {object} options
* @returns {Promise<Message>}
*/
async reply(content, chatId, options={}) {
if (!chatId) {
chatId = this._getChatId();
}
options = {
...options,
quotedMessageId: this.id._serialized
};
return this.client.sendMessage(chatId, content, options);
}
/**
* Downloads and returns the attatched message media
* @returns {Promise<MessageMedia>}
*/
async downloadMedia() {
if (!this.hasMedia) {
return undefined;
}
const result = await this.client.pupPage.evaluate(async (msgId) => {
const msg = window.Store.Msg.get(msgId);
if(msg.mediaData.mediaStage != 'RESOLVED') {
// try to resolve media
await msg.downloadMedia(true, 1);
}
if(msg.mediaData.mediaStage.includes('ERROR')) {
// media could not be downloaded
return undefined;
}
const buffer = await window.WWebJS.downloadBuffer(msg.clientUrl);
const decrypted = await window.Store.CryptoLib.decryptE2EMedia(msg.type, buffer, msg.mediaKey, msg.mimetype);
const data = await window.WWebJS.readBlobAsync(decrypted._blob);
return {
data: data.split(',')[1],
mimetype: msg.mimetype,
filename: msg.filename
};
}, this.id._serialized);
if(!result) return undefined;
return new MessageMedia(result.mimetype, result.data, result.filename);
}
/**
* Deletes a message from the chat
* @param {?boolean} everyone If true and the message is sent by the current user, will delete it for everyone in the chat.
*/
async delete(everyone) {
await this.client.pupPage.evaluate((msgId, everyone) => {
let msg = window.Store.Msg.get(msgId);
if(everyone && msg.id.fromMe && msg.canRevoke()) {
return window.Store.Cmd.sendRevokeMsgs(msg.chat, [msg], true);
}
return window.Store.Cmd.sendDeleteMsgs(msg.chat, [msg], true);
}, this.id._serialized, everyone);
}
}
module.exports = Message;

View File

@@ -1,48 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const mime = require('mime');
/**
* Media attached to a message
* @param {string} mimetype MIME type of the attachment
* @param {string} data Base64-encoded data of the file
* @param {?string} filename Document file name
*/
class MessageMedia {
constructor(mimetype, data, filename) {
/**
* MIME type of the attachment
* @type {string}
*/
this.mimetype = mimetype;
/**
* Base64 encoded data that represents the file
* @type {string}
*/
this.data = data;
/**
* Name of the file (for documents)
* @type {?string}
*/
this.filename = filename;
}
/**
* Creates a MessageMedia instance from a local file path
* @param {string} filePath
* @returns {MessageMedia}
*/
static fromFilePath(filePath) {
const b64data = fs.readFileSync(filePath, {encoding: 'base64'});
const mimetype = mime.getType(filePath);
const filename = path.basename(filePath);
return new MessageMedia(mimetype, b64data, filename);
}
}
module.exports = MessageMedia;

View File

@@ -1,13 +0,0 @@
'use strict';
const Chat = require('./Chat');
/**
* Represents a Private Chat on WhatsApp
* @extends {Chat}
*/
class PrivateChat extends Chat {
}
module.exports = PrivateChat;

View File

@@ -1,13 +0,0 @@
'use strict';
const Contact = require('./Contact');
/**
* Represents a Private Contact on WhatsApp
* @extends {Contact}
*/
class PrivateContact extends Contact {
}
module.exports = PrivateContact;

View File

@@ -1,14 +0,0 @@
module.exports = {
Base: require('./Base'),
BusinessContact: require('./BusinessContact'),
Chat: require('./Chat'),
ClientInfo: require('./ClientInfo'),
Contact: require('./Contact'),
GroupChat: require('./GroupChat'),
Location: require('./Location'),
Message: require('./Message'),
MessageMedia: require('./MessageMedia'),
PrivateChat: require('./PrivateChat'),
PrivateContact: require('./PrivateContact'),
GroupNotification: require('./GroupNotification')
};

View File

@@ -1,134 +0,0 @@
'use strict';
exports.WhatsWebURL = 'https://web.whatsapp.com/';
exports.DefaultOptions = {
puppeteer: {
headless: true,
defaultViewport: null
},
session: false,
qrTimeoutMs: 45000,
qrRefreshIntervalMs: 20000,
authTimeoutMs: 45000,
takeoverOnConflict: false,
takeoverTimeoutMs: 0,
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36'
};
/**
* Client status
* @readonly
* @enum {number}
*/
exports.Status = {
INITIALIZING: 0,
AUTHENTICATING: 1,
READY: 3
};
/**
* Events that can be emitted by the client
* @readonly
* @enum {string}
*/
exports.Events = {
AUTHENTICATED: 'authenticated',
AUTHENTICATION_FAILURE: 'auth_failure',
READY: 'ready',
MESSAGE_RECEIVED: 'message',
MESSAGE_CREATE: 'message_create',
MESSAGE_REVOKED_EVERYONE: 'message_revoke_everyone',
MESSAGE_REVOKED_ME: 'message_revoke_me',
MESSAGE_ACK: 'message_ack',
MEDIA_UPLOADED: 'media_uploaded',
GROUP_JOIN: 'group_join',
GROUP_LEAVE: 'group_leave',
GROUP_UPDATE: 'group_update',
QR_RECEIVED: 'qr',
DISCONNECTED: 'disconnected',
STATE_CHANGED: 'change_state',
BATTERY_CHANGED: 'change_battery'
};
/**
* Message types
* @readonly
* @enum {string}
*/
exports.MessageTypes = {
TEXT: 'chat',
AUDIO: 'audio',
VOICE: 'ptt',
IMAGE: 'image',
VIDEO: 'video',
DOCUMENT: 'document',
STICKER: 'sticker',
LOCATION: 'location',
CONTACT_CARD: 'vcard',
CONTACT_CARD_MULTI: 'multi_vcard',
REVOKED: 'revoked',
UNKNOWN: 'unknown'
};
/**
* Group notification types
* @readonly
* @enum {string}
*/
exports.GroupNotificationTypes = {
ADD: 'add',
INVITE: 'invite',
REMOVE: 'remove',
LEAVE: 'leave',
SUBJECT: 'subject',
DESCRIPTION: 'description',
PICTURE: 'picture',
ANNOUNCE: 'announce',
RESTRICT: 'restrict',
};
/**
* Chat types
* @readonly
* @enum {string}
*/
exports.ChatTypes = {
SOLO: 'solo',
GROUP: 'group',
UNKNOWN: 'unknown'
};
/**
* WhatsApp state
* @readonly
* @enum {string}
*/
exports.WAState = {
CONFLICT: 'CONFLICT',
CONNECTED: 'CONNECTED',
DEPRECATED_VERSION: 'DEPRECATED_VERSION',
OPENING: 'OPENING',
PAIRING: 'PAIRING',
PROXYBLOCK: 'PROXYBLOCK',
SMB_TOS_BLOCK: 'SMB_TOS_BLOCK',
TIMEOUT: 'TIMEOUT',
TOS_BLOCK: 'TOS_BLOCK',
UNLAUNCHED: 'UNLAUNCHED',
UNPAIRED: 'UNPAIRED',
UNPAIRED_IDLE: 'UNPAIRED_IDLE'
};
/**
* Message ACK
* @readonly
* @enum {number}
*/
exports.MessageAck = {
ACK_ERROR: -1,
ACK_PENDING: 0,
ACK_SERVER: 1,
ACK_DEVICE: 2,
ACK_READ: 3,
ACK_PLAYED: 4,
};

View File

@@ -1,321 +0,0 @@
'use strict';
// Exposes the internal Store to the WhatsApp Web client
exports.ExposeStore = (moduleRaidStr) => {
eval('var moduleRaid = ' + moduleRaidStr);
// eslint-disable-next-line no-undef
window.mR = moduleRaid();
window.Store = window.mR.findModule('Chat')[1].default;
window.Store.AppState = window.mR.findModule('STREAM')[0].default;
window.Store.Conn = window.mR.findModule('Conn')[0].default;
window.Store.CryptoLib = window.mR.findModule('decryptE2EMedia')[0];
window.Store.Wap = window.mR.findModule('Wap')[0].default;
window.Store.SendSeen = window.mR.findModule('sendSeen')[0];
window.Store.SendClear = window.mR.findModule('sendClear')[0];
window.Store.SendDelete = window.mR.findModule('sendDelete')[0];
window.Store.genId = window.mR.findModule((module) => module.default && typeof module.default === 'function' && module.default.toString().match(/crypto/))[0].default;
window.Store.SendMessage = window.mR.findModule('addAndSendMsgToChat')[0];
window.Store.MsgKey = window.mR.findModule((module) => module.default && module.default.fromString)[0].default;
window.Store.Invite = window.mR.findModule('sendJoinGroupViaInvite')[0];
window.Store.OpaqueData = window.mR.findModule('getOrCreateOpaqueDataForPath')[0];
window.Store.MediaPrep = window.mR.findModule('MediaPrep')[0];
window.Store.MediaObject = window.mR.findModule('getOrCreateMediaObject')[0];
window.Store.MediaUpload = window.mR.findModule('uploadMedia')[0];
window.Store.Cmd = window.mR.findModule('Cmd')[0].default;
window.Store.MediaTypes = window.mR.findModule('msgToMediaType')[0];
window.Store.UserConstructor = window.mR.findModule((module) => (module.default && module.default.prototype && module.default.prototype.isServer && module.default.prototype.isUser) ? module.default : null)[0].default;
window.Store.Validators = window.mR.findModule('findLinks')[0];
window.Store.WidFactory = window.mR.findModule('createWid')[0];
};
exports.LoadUtils = () => {
window.WWebJS = {};
window.WWebJS.getNumberId = async (id) => {
let result = await window.Store.Wap.queryExist(id);
if (result.jid === undefined)
throw 'The number provided is not a registered whatsapp user';
return result.jid;
};
window.WWebJS.sendSeen = async (chatId) => {
let chat = window.Store.Chat.get(chatId);
if (chat !== undefined) {
await window.Store.SendSeen.sendSeen(chat, false);
return true;
}
return false;
};
window.WWebJS.sendMessage = async (chat, content, options = {}) => {
let attOptions = {};
if (options.attachment) {
attOptions = await window.WWebJS.processMediaData(options.attachment, options.sendAudioAsVoice);
content = attOptions.preview;
delete options.attachment;
}
let quotedMsgOptions = {};
if (options.quotedMessageId) {
let quotedMessage = window.Store.Msg.get(options.quotedMessageId);
if (quotedMessage.canReply()) {
quotedMsgOptions = quotedMessage.msgContextInfo(chat);
}
delete options.quotedMessageId;
}
if (options.mentionedJidList) {
options.mentionedJidList = options.mentionedJidList.map(cId => window.Store.Contact.get(cId).id);
}
let locationOptions = {};
if (options.location) {
locationOptions = {
type: 'location',
loc: options.location.description,
lat: options.location.latitude,
lng: options.location.longitude
};
delete options.location;
}
if (options.linkPreview) {
delete options.linkPreview;
const link = window.Store.Validators.findLink(content);
if (link) {
const preview = await window.Store.Wap.queryLinkPreview(link.url);
preview.preview = true;
preview.subtype = 'url';
options = { ...options, ...preview };
}
}
const newMsgId = new window.Store.MsgKey({
from: window.Store.Conn.me,
to: chat.id,
id: window.Store.genId(),
});
const message = {
...options,
id: newMsgId,
ack: 0,
body: content,
from: window.Store.Conn.me,
to: chat.id,
local: true,
self: 'out',
t: parseInt(new Date().getTime() / 1000),
isNewMsg: true,
type: 'chat',
...locationOptions,
...attOptions,
...quotedMsgOptions
};
await window.Store.SendMessage.addAndSendMsgToChat(chat, message);
return window.Store.Msg.get(newMsgId._serialized);
};
window.WWebJS.processMediaData = async (mediaInfo, forceVoice) => {
const file = window.WWebJS.mediaInfoToFile(mediaInfo);
const mData = await window.Store.OpaqueData.default.createFromData(file, file.type);
const mediaPrep = window.Store.MediaPrep.prepRawMedia(mData, {});
const mediaData = await mediaPrep.waitForPrep();
const mediaObject = window.Store.MediaObject.getOrCreateMediaObject(mediaData.filehash);
const mediaType = window.Store.MediaTypes.msgToMediaType({
type: mediaData.type,
isGif: mediaData.isGif
});
if(forceVoice && mediaData.type === 'audio') {
mediaData.type = 'ptt';
}
if (!(mediaData.mediaBlob instanceof window.Store.OpaqueData.default)) {
mediaData.mediaBlob = await window.Store.OpaqueData.default.createFromData(mediaData.mediaBlob, mediaData.mediaBlob.type);
}
mediaData.renderableUrl = mediaData.mediaBlob.url();
mediaObject.consolidate(mediaData.toJSON());
mediaData.mediaBlob.autorelease();
const uploadedMedia = await window.Store.MediaUpload.uploadMedia({ mimetype: mediaData.mimetype, mediaObject, mediaType });
if (!uploadedMedia) {
throw new Error('upload failed: media entry was not created');
}
mediaData.set({
clientUrl: uploadedMedia.mmsUrl,
directPath: uploadedMedia.directPath,
mediaKey: uploadedMedia.mediaKey,
mediaKeyTimestamp: uploadedMedia.mediaKeyTimestamp,
filehash: mediaObject.filehash,
uploadhash: uploadedMedia.uploadHash,
size: mediaObject.size,
streamingSidecar: uploadedMedia.sidecar,
firstFrameSidecar: uploadedMedia.firstFrameSidecar
});
return mediaData;
};
window.WWebJS.getChatModel = chat => {
let res = chat.serialize();
res.isGroup = chat.isGroup;
res.formattedTitle = chat.formattedTitle;
if (chat.groupMetadata) {
res.groupMetadata = chat.groupMetadata.serialize();
}
return res;
};
window.WWebJS.getChat = chatId => {
const chat = window.Store.Chat.get(chatId);
return window.WWebJS.getChatModel(chat);
};
window.WWebJS.getChats = () => {
const chats = window.Store.Chat.models;
return chats.map(chat => window.WWebJS.getChatModel(chat));
};
window.WWebJS.getContactModel = contact => {
let res = contact.serialize();
res.isBusiness = contact.isBusiness;
if (contact.businessProfile) {
res.businessProfile = contact.businessProfile.serialize();
}
res.isMe = contact.isMe;
res.isUser = contact.isUser;
res.isGroup = contact.isGroup;
res.isWAContact = contact.isWAContact;
res.isMyContact = contact.isMyContact;
res.userid = contact.userid;
return res;
};
window.WWebJS.getContact = contactId => {
const contact = window.Store.Contact.get(contactId);
return window.WWebJS.getContactModel(contact);
};
window.WWebJS.getContacts = () => {
const contacts = window.Store.Contact.models;
return contacts.map(contact => window.WWebJS.getContactModel(contact));
};
window.WWebJS.mediaInfoToFile = ({ data, mimetype, filename }) => {
const binaryData = atob(data);
const buffer = new ArrayBuffer(binaryData.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < binaryData.length; i++) {
view[i] = binaryData.charCodeAt(i);
}
const blob = new Blob([buffer], { type: mimetype });
return new File([blob], filename, {
type: mimetype,
lastModified: Date.now()
});
};
window.WWebJS.downloadBuffer = (url) => {
return new Promise(function (resolve, reject) {
let xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (xhr.status == 200) {
resolve(xhr.response);
} else {
reject({
status: this.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function () {
reject({
status: this.status,
statusText: xhr.statusText
});
};
xhr.send(null);
});
};
window.WWebJS.readBlobAsync = (blob) => {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
};
window.WWebJS.sendClearChat = async (chatId) => {
let chat = window.Store.Chat.get(chatId);
if (chat !== undefined) {
await window.Store.SendClear.sendClear(chat, false);
return true;
}
return false;
};
window.WWebJS.sendDeleteChat = async (chatId) => {
let chat = window.Store.Chat.get(chatId);
if (chat !== undefined) {
await window.Store.SendDelete.sendDelete(chat);
return true;
}
return false;
};
window.WWebJS.sendChatstate = async (state, chatId) => {
switch(state) {
case 'typing':
await window.Store.Wap.sendChatstateComposing(chatId);
break;
case 'recording':
await window.Store.Wap.sendChatstateRecording(chatId);
break;
case 'stop':
await window.Store.Wap.sendChatstatePaused(chatId);
break;
default:
throw 'Invalid chatstate';
}
return true;
};
};
exports.MarkAllRead = () => {
let Chats = window.Store.Chat.models;
for (let chatIndex in Chats) {
if (isNaN(chatIndex)) {
continue;
}
let chat = Chats[chatIndex];
if (chat.unreadCount > 0) {
chat.markSeen();
window.Store.Wap.sendConversationSeen(chat.id, chat.getLastMsgKeyForAction(), chat.unreadCount - chat.pendingSeenCount);
}
}
};

View File

@@ -1,56 +0,0 @@
'use strict';
/**
* Interface Controller
*/
class InterfaceController {
constructor(props) {
this.pupPage = props.pupPage;
}
/**
* Opens the Chat Window
* @param {string} chatId ID of the chat window that will be opened
*/
async openChatWindow(chatId) {
await this.pupPage.evaluate(async chatId => {
let chat = await window.Store.Chat.get(chatId);
await window.Store.Cmd.openChatAt(chat);
}, chatId);
}
/**
* Opens the Chat Drawer
* @param {string} chatId ID of the chat drawer that will be opened
*/
async openChatDrawer(chatId) {
await this.pupPage.evaluate(async chatId => {
let chat = await window.Store.Chat.get(chatId);
await window.Store.Cmd.chatInfoDrawer(chat);
}, chatId);
}
/**
* Opens the Message Drawer
* @param {string} msgId ID of the message drawer that will be opened
*/
async openMessageDrawer(msgId) {
await this.pupPage.evaluate(async msgId => {
let msg = await window.Store.Msg.get(msgId);
await window.Store.Cmd.msgInfoDrawer(msg);
}, msgId);
}
/**
* Closes the Right Drawer
*/
async closeRightDrawer() {
await this.pupPage.evaluate(async () => {
await window.Store.Cmd.closeDrawerRight();
});
}
}
module.exports = InterfaceController;

View File

@@ -1,35 +0,0 @@
'use strict';
const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
/**
* Utility methods
*/
class Util {
constructor() {
throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
}
/**
* Sets default properties on an object that aren't already specified.
* @param {Object} def Default properties
* @param {Object} given Object to assign defaults to
* @returns {Object}
* @private
*/
static mergeDefault(def, given) {
if (!given) return def;
for (const key in def) {
if (!has(given, key) || given[key] === undefined) {
given[key] = def[key];
} else if (given[key] === Object(given[key])) {
given[key] = Util.mergeDefault(def[key], given[key]);
}
}
return given;
}
}
module.exports = Util;