diff --git a/src/Binary/Constants.ts b/src/Binary/Constants.ts index f4de64f..cb817a5 100644 --- a/src/Binary/Constants.ts +++ b/src/Binary/Constants.ts @@ -199,7 +199,7 @@ export namespace WA { 'recent', ] export const Message = Coding.WebMessageInfo - export type NodeAttributes = Record | string | null + export type NodeAttributes = { [key: string]: string } | string | null export type NodeData = Array | any | null export type Node = [string, NodeAttributes, NodeData] } diff --git a/src/WAClient/Base.ts b/src/WAClient/Base.ts index 4b3fe81..eec6243 100644 --- a/src/WAClient/Base.ts +++ b/src/WAClient/Base.ts @@ -1,5 +1,5 @@ import WAConnection from '../WAConnection/WAConnection' -import { MessageStatus, MessageStatusUpdate, PresenceUpdate, Presence, ChatModification } from './Constants' +import { MessageStatus, MessageStatusUpdate, PresenceUpdate, Presence, ChatModification, WABroadcastListInfo } from './Constants' import { WAMessage, WANode, @@ -95,17 +95,33 @@ export default class WhatsAppWebBase extends WAConnection { const response = await this.queryExpecting200(['query', 'ProfilePicThumb', jid || this.userMetaData.id]) return response.eurl as string } + /** Query broadcast list info */ + async getBroadcastListInfo(jid: string) { return this.queryExpecting200(['query', 'contact', jid]) as Promise } /** Get your contacts */ async getContacts() { const json = ['query', { epoch: this.msgCount.toString(), type: 'contacts' }, null] - const response = await this.query(json, [WAMetric.group, WAFlag.ignore]) // this has to be an encrypted query - console.log(response) + const response = await this.query(json, [6, WAFlag.ignore]) // this has to be an encrypted query return response } + /** Get the stories of your contacts */ + async getStories() { + const json = ['query', { epoch: this.msgCount.toString(), type: 'status' }, null] + const response = await this.queryExpecting200(json, [30, WAFlag.ignore]) as WANode + if (Array.isArray(response[2])) { + return response[2].map (row => ( + { + unread: row[1]?.unread, + count: row[1]?.count, + messages: Array.isArray(row[2]) ? row[2].map (m => m[2]) : [] + } as {unread: number, count: number, messages: WAMessage[]} + )) + } + return [] + } /** Fetch your chats */ - getChats() { + async getChats() { const json = ['query', { epoch: this.msgCount.toString(), type: 'chat' }, null] - return this.query(json, [WAMetric.group, WAFlag.ignore]) // this has to be an encrypted query + return this.query(json, [5, WAFlag.ignore]) // this has to be an encrypted query } /** * Check if your phone is connected @@ -189,86 +205,4 @@ export default class WhatsAppWebBase extends WAConnection { const json = ['action', {epoch: this.msgCount.toString(), type: 'set'}, nodes] return this.queryExpecting200(json, [WAMetric.group, WAFlag.ignore]) as Promise<{status: number}> } - /** Generic function for group queries */ - async groupQuery(type: string, jid?: string, subject?: string, participants?: string[]) { - const json: WANode = [ - 'group', - { - author: this.userMetaData.id, - id: generateMessageTag(), - type: type, - jid: jid, - subject: subject, - }, - participants ? participants.map((str) => ['participant', { jid: str }, null]) : [], - ] - const q = ['action', { type: 'set', epoch: this.msgCount.toString() }, [json]] - return this.queryExpecting200(q, [WAMetric.group, WAFlag.ignore]) - } - /** Get the metadata of the group */ - groupMetadata = (jid: string) => this.queryExpecting200(['query', 'GroupMetadata', jid]) as Promise - /** Get the metadata (works after you've left the group also) */ - groupCreatorAndParticipants = async (jid: string) => { - const query = ['query', {type: 'group', jid: jid, epoch: this.msgCount.toString()}, null] - const response = await this.queryExpecting200(query, [WAMetric.group, WAFlag.ignore]) - const json = response[2][0] - const creatorDesc = json[1] - const participants = json[2] ? json[2].filter (item => item[0] === 'participant') : [] - const description = json[2] ? json[2].find (item => item[0] === 'description') : null - return { - id: jid, - owner: creatorDesc?.creator, - creator: creatorDesc?.creator, - creation: parseInt(creatorDesc?.create), - subject: null, - desc: description ? description[2].toString('utf-8') : null, - participants: participants.map (item => ({ id: item[1].jid, isAdmin: item[1].type==='admin' })) - } as WAGroupMetadata - } - /** - * Create a group - * @param title like, the title of the group - * @param participants people to include in the group - */ - groupCreate = (title: string, participants: string[]) => - this.groupQuery('create', null, title, participants) as Promise - /** - * Leave a group - * @param jid the ID of the group - */ - groupLeave = (jid: string) => this.groupQuery('leave', jid) as Promise<{ status: number }> - /** - * Update the subject of the group - * @param {string} jid the ID of the group - * @param {string} title the new title of the group - */ - groupUpdateSubject = (jid: string, title: string) => - this.groupQuery('subject', jid, title) as Promise<{ status: number }> - /** - * Add somebody to the group - * @param jid the ID of the group - * @param participants the people to add - */ - groupAdd = (jid: string, participants: string[]) => - this.groupQuery('add', jid, null, participants) as Promise - /** - * Remove somebody from the group - * @param jid the ID of the group - * @param participants the people to remove - */ - groupRemove = (jid: string, participants: string[]) => - this.groupQuery('remove', jid, null, participants) as Promise - /** - * Make someone admin on the group - * @param jid the ID of the group - * @param participants the people to make admin - */ - groupMakeAdmin = (jid: string, participants: string[]) => - this.groupQuery('promote', jid, null, participants) as Promise - /** Get the invite link of the given group */ - async groupInviteCode(jid: string) { - const json = ['query', 'inviteCode', jid] - const response = await this.queryExpecting200(json) - return response.code as string - } } diff --git a/src/WAClient/Constants.ts b/src/WAClient/Constants.ts index a3a1113..43a73b1 100644 --- a/src/WAClient/Constants.ts +++ b/src/WAClient/Constants.ts @@ -75,6 +75,11 @@ export interface MessageOptions { validateID?: boolean, filename?: string } +export interface WABroadcastListInfo { + status: number + name: string + recipients?: {id: string}[] +} export interface MessageInfo { reads: {jid: string, t: string}[] deliveries: {jid: string, t: string}[] diff --git a/src/WAClient/Groups.ts b/src/WAClient/Groups.ts new file mode 100644 index 0000000..ccbd67f --- /dev/null +++ b/src/WAClient/Groups.ts @@ -0,0 +1,88 @@ +import WhatsAppWebBase from './Base' +import { WAMessage, WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification } from '../WAConnection/Constants' +import { generateMessageTag } from '../WAConnection/Utils' + +export default class WhatsAppWebGroups extends WhatsAppWebBase { + /** Generic function for group queries */ + async groupQuery(type: string, jid?: string, subject?: string, participants?: string[]) { + const json: WANode = [ + 'group', + { + author: this.userMetaData.id, + id: generateMessageTag(), + type: type, + jid: jid, + subject: subject, + }, + participants ? participants.map((str) => ['participant', { jid: str }, null]) : [], + ] + const q = ['action', { type: 'set', epoch: this.msgCount.toString() }, [json]] + return this.queryExpecting200(q, [WAMetric.group, WAFlag.ignore]) + } + /** Get the metadata of the group */ + groupMetadata = (jid: string) => this.queryExpecting200(['query', 'GroupMetadata', jid]) as Promise + /** Get the metadata (works after you've left the group also) */ + groupCreatorAndParticipants = async (jid: string) => { + const query = ['query', {type: 'group', jid: jid, epoch: this.msgCount.toString()}, null] + const response = await this.queryExpecting200(query, [WAMetric.group, WAFlag.ignore]) + const json = response[2][0] + const creatorDesc = json[1] + const participants = json[2] ? json[2].filter (item => item[0] === 'participant') : [] + const description = json[2] ? json[2].find (item => item[0] === 'description') : null + return { + id: jid, + owner: creatorDesc?.creator, + creator: creatorDesc?.creator, + creation: parseInt(creatorDesc?.create), + subject: null, + desc: description ? description[2].toString('utf-8') : null, + participants: participants.map (item => ({ id: item[1].jid, isAdmin: item[1].type==='admin' })) + } as WAGroupMetadata + } + /** + * Create a group + * @param title like, the title of the group + * @param participants people to include in the group + */ + groupCreate = (title: string, participants: string[]) => + this.groupQuery('create', null, title, participants) as Promise + /** + * Leave a group + * @param jid the ID of the group + */ + groupLeave = (jid: string) => this.groupQuery('leave', jid) as Promise<{ status: number }> + /** + * Update the subject of the group + * @param {string} jid the ID of the group + * @param {string} title the new title of the group + */ + groupUpdateSubject = (jid: string, title: string) => + this.groupQuery('subject', jid, title) as Promise<{ status: number }> + /** + * Add somebody to the group + * @param jid the ID of the group + * @param participants the people to add + */ + groupAdd = (jid: string, participants: string[]) => + this.groupQuery('add', jid, null, participants) as Promise + /** + * Remove somebody from the group + * @param jid the ID of the group + * @param participants the people to remove + */ + groupRemove = (jid: string, participants: string[]) => + this.groupQuery('remove', jid, null, participants) as Promise + /** + * Make someone admin on the group + * @param jid the ID of the group + * @param participants the people to make admin + */ + groupMakeAdmin = (jid: string, participants: string[]) => + this.groupQuery('promote', jid, null, participants) as Promise + /** Get the invite link of the given group */ + async groupInviteCode(jid: string) { + const json = ['query', 'inviteCode', jid] + const response = await this.queryExpecting200(json) + return response.code as string + } +} \ No newline at end of file diff --git a/src/WAClient/Messages.ts b/src/WAClient/Messages.ts index 515f569..8643724 100644 --- a/src/WAClient/Messages.ts +++ b/src/WAClient/Messages.ts @@ -1,4 +1,4 @@ -import WhatsAppWebBase from './Base' +import WhatsAppWebGroups from './Groups' import fetch from 'node-fetch' import { MessageOptions, @@ -18,7 +18,7 @@ import { WAMessageContent, WAMetric, WAFlag, WANode, WAMessage } from '../WAConn import { validateJIDForSending, generateThumbnail, getMediaKeys } from './Utils' import { proto } from '../../WAMessage/WAMessage' -export default class WhatsAppWebMessages extends WhatsAppWebBase { +export default class WhatsAppWebMessages extends WhatsAppWebGroups { /** Get the message info, who has read it, who its been delivered to */ async messageInfo (jid: string, messageID: string) { const query = ['query', {type: 'message_info', index: messageID, jid: jid, epoch: this.msgCount.toString()}, null] @@ -295,4 +295,43 @@ export default class WhatsAppWebMessages extends WhatsAppWebBase { const response = await this.queryExpecting200(json, [WAMetric.message, WAFlag.ignore], null, messageJSON.key.id) return { status: response.status as number, messageID: messageJSON.key.id } as WASendMessageResponse } + /** + * Load the entire friggin conversation with a group or person + * @param onMessage callback for every message retreived + * @param [chunkSize] the number of messages to load in a single request + * @param [mostRecentFirst] retreive the most recent message first or retreive from the converation start + */ + loadEntireConversation(jid: string, onMessage: (m: WAMessage) => void, chunkSize = 25, mostRecentFirst = true) { + let offsetID = null + const loadMessage = async () => { + const json = await this.loadConversation(jid, chunkSize, offsetID, mostRecentFirst) + // callback with most recent message first (descending order of date) + let lastMessage + if (mostRecentFirst) { + for (let i = json.length - 1; i >= 0; i--) { + onMessage(json[i]) + lastMessage = json[i] + } + } else { + for (let i = 0; i < json.length; i++) { + onMessage(json[i]) + lastMessage = json[i] + } + } + // if there are still more messages + if (json.length >= chunkSize) { + offsetID = lastMessage.key // get the last message + return new Promise((resolve, reject) => { + // send query after 200 ms + setTimeout(() => loadMessage().then(resolve).catch(reject), 200) + }) + } + } + return loadMessage() as Promise + } + /** Generic function for action, set queries */ + async setQuery (nodes: WANode[]) { + const json = ['action', {epoch: this.msgCount.toString(), type: 'set'}, nodes] + return this.queryExpecting200(json, [WAMetric.group, WAFlag.ignore]) as Promise<{status: number}> + } } diff --git a/src/WAClient/Tests.ts b/src/WAClient/Tests.ts index 57bc3a1..9648499 100644 --- a/src/WAClient/Tests.ts +++ b/src/WAClient/Tests.ts @@ -70,13 +70,14 @@ WAClientTest('Messages', (client) => { await client.clearMessage (messages[0].key) }) it ('should load convo', async () => { - const [chats] = await client.receiveChatsAndContacts () + /*const [chats] = await client.receiveChatsAndContacts () for (var i in chats) { if (chats[i].jid.includes('@g.us')) { console.log (chats[i].jid) const data = await client.groupCreatorAndParticipants (chats[i].jid) } - } + }*/ + }) }) @@ -116,6 +117,9 @@ WAClientTest('Misc', (client) => { assert.ok(response.status) assert.strictEqual(typeof response.status, 'string') }) + it('should return the stories', async () => { + await client.getStories() + }) it('should return the profile picture', async () => { const response = await client.getProfilePicture(testJid) assert.ok(response) diff --git a/src/WAConnection/Utils.ts b/src/WAConnection/Utils.ts index bb57f60..a327e56 100644 --- a/src/WAConnection/Utils.ts +++ b/src/WAConnection/Utils.ts @@ -77,11 +77,11 @@ export function errorOnNon200Status(p: Promise) { } export function decryptWA (message: any, macKey: Buffer, encKey: Buffer, decoder: Decoder, fromMe: boolean=false): [string, Object, [number, number]?] { - const commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message - if (commaIndex < 0) { - // if there was no comma, then this message must be not be valid - throw Error ('invalid message: ' + message) - } + let commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message + + if (commaIndex < 0) throw Error ('invalid message: ' + message) // if there was no comma, then this message must be not be valid + + if (message[commaIndex+1] === ',') commaIndex += 1 let data = message.slice(commaIndex+1, message.length) // get the message tag. // If a query was done, the server will respond with the same message tag we sent the query with