From 6aa1bd85ab5fed25c66e5f0cb5b68112bc9f4c40 Mon Sep 17 00:00:00 2001 From: RendyCraft047 <75566525+RC047@users.noreply.github.com> Date: Sat, 13 Dec 2025 11:34:55 +0700 Subject: [PATCH] feat: whatsapp stories implementation (#3994) * support sending message from status broadcast * add to index.d.ts * add method to contact * new modules * Update Utils.js * add for the examples * add color assertion function * finishing.. * added waveforms for audio messages and minor fixes * fix eslint * refactor reorganize the store to include additional modules. * adapt * the last one. --------- Co-authored-by: Rajeh Taher --- example.js | 7 ++++ index.d.ts | 10 +++++- src/Client.js | 65 ++++++++++++++++++++++++++++++++++++-- src/structures/Contact.js | 9 +++++- src/util/Injected/Store.js | 10 ++++-- src/util/Injected/Utils.js | 63 +++++++++++++++++++++++++++++++++--- 6 files changed, 152 insertions(+), 12 deletions(-) diff --git a/example.js b/example.js index 65314ba..c2eb737 100644 --- a/example.js +++ b/example.js @@ -523,6 +523,13 @@ client.on('message', async msg => { // NOTE: this action will take effect after you restart the client. const backgroundSync = await client.setBackgroundSync(true); console.log(backgroundSync); + } else if (msg.body === '!postStatus') { + await client.sendMessage('status@broadcast', 'Hello there!'); + // send with a different style + await client.sendMessage('status@broadcast', 'Hello again! Looks different?', { + fontStyle: 1, + backgroundColor: '#0b3296' + }); } }); diff --git a/index.d.ts b/index.d.ts index 1b6653a..5f4c3f6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -113,9 +113,15 @@ declare namespace WAWebJS { /** Get all current Labels */ getLabels(): Promise - + /** Get all current Broadcasts */ getBroadcasts(): Promise + + /** Get broadcast instance by current user ID */ + getBroadcastById(contactId: string): Promise + + /** Revoke current own status messages */ + revokeStatusMessage(messageId: string): Promise /** Change labels in chats */ addOrRemoveLabels(labelIds: Array, chatIds: Array): Promise @@ -1569,6 +1575,8 @@ declare namespace WAWebJS { /** Gets the Contact's common groups with you. Returns empty array if you don't have any common group. */ getCommonGroups: () => Promise + /** Gets the Contact's current status broadcast. */ + getBroadcast: () => Promise } export interface ContactId { diff --git a/src/Client.js b/src/Client.js index 9cc3375..ead8a50 100644 --- a/src/Client.js +++ b/src/Client.js @@ -948,9 +948,10 @@ class Client extends EventEmitter { */ async sendMessage(chatId, content, options = {}) { const isChannel = /@\w*newsletter\b/.test(chatId); + const isStatus = /@\w*broadcast\b/.test(chatId); if (isChannel && [ - options.sendMediaAsDocument, options.quotedMessageId, + options.sendMediaAsDocument, options.quotedMessageId, options.parseVCards, options.isViewOnce, content instanceof Location, content instanceof Contact, content instanceof Buttons, content instanceof List, @@ -958,6 +959,16 @@ class Client extends EventEmitter { ].includes(true)) { console.warn('The message type is currently not supported for sending in channels,\nthe supported message types are: text, image, sticker, gif, video, voice and poll.'); return null; + + } else if (isStatus && [ + options.sendMediaAsDocument, options.quotedMessageId, + options.parseVCards, options.isViewOnce, options.sendMediaAsSticker, + content instanceof Location, content instanceof Contact, + content instanceof Poll, content instanceof Buttons, content instanceof List, + Array.isArray(content) && content.length > 0 && content[0] instanceof Contact + ].includes(true)) { + console.warn('The message type is currently not supported for sending in status broadcast,\nthe supported message types are: text, image, gif, audio and video.'); + return null; } if (options.mentions) { @@ -1193,7 +1204,12 @@ class Client extends EventEmitter { return ContactFactory.create(this, contact); } - + + /** + * Get message by ID + * @param {string} messageId + * @returns {Promise} + */ async getMessageById(messageId) { const msg = await this.pupPage.evaluate(async messageId => { let msg = window.Store.Msg.get(messageId); @@ -1970,7 +1986,7 @@ class Client extends EventEmitter { return labels.map(data => new Label(this, data)); } - + /** * Get all current Broadcast * @returns {Promise>} @@ -1982,6 +1998,49 @@ class Client extends EventEmitter { return broadcasts.map(data => new Broadcast(this, data)); } + /** + * Get broadcast instance by current user ID + * @param {string} contactId + * @returns {Promise} + */ + async getBroadcastById(contactId) { + const broadcast = await this.pupPage.evaluate(async (userId) => { + let status; + try { + status = window.Store.Status.get(userId); + if (!status) { + status = await window.Store.Status.find(userId); + } + } catch { + status = null; + } + + if (status) return window.WWebJS.getStatusModel(status); + }, contactId); + return new Broadcast(this, broadcast); + } + + /** + * Revoke current own status messages + * @param {string} messageId + * @returns {Promise} + */ + async revokeStatusMessage(messageId) { + return await this.pupPage.evaluate(async (msgId) => { + const status = window.Store.Status.getMyStatus(); + if (!status) return; + + const msg = + window.Store.Msg.get(msgId) || (await window.Store.Msg.getMessagesById([msgId]))?.messages?.[0]; + if (!msg) return; + + if (!msg.id.fromMe || !msg.id.remote.isStatus()) + throw 'Invalid usage! Can only revoke the message its from own status broadcast'; + + return await window.Store.StatusUtils.sendStatusRevokeMsgAction(status, msg); + }, messageId); + } + /** * Get Label instance by ID * @param {string} labelId diff --git a/src/structures/Contact.js b/src/structures/Contact.js index f071f7a..024ad02 100644 --- a/src/structures/Contact.js +++ b/src/structures/Contact.js @@ -202,7 +202,14 @@ class Contact extends Base { async getCommonGroups() { return await this.client.getCommonGroups(this.id._serialized); } - + + /** + * Gets the Contact's current status broadcast. + * @returns {Promise} + */ + async getBroadcast() { + return await this.client.getBroadcastById(this.id._serialized); + } } module.exports = Contact; diff --git a/src/util/Injected/Store.js b/src/util/Injected/Store.js index 08fe745..3f2c8c2 100644 --- a/src/util/Injected/Store.js +++ b/src/util/Injected/Store.js @@ -54,6 +54,7 @@ exports.ExposeStore = () => { window.Store.MediaObject = window.require('WAWebMediaStorage'); window.Store.MediaTypes = window.require('WAWebMmsMediaTypes'); window.Store.MediaUpload = window.require('WAWebMediaMmsV4Upload'); + window.Store.MediaUpdate = window.require('WAWebMediaUpdateMsg'); window.Store.MsgKey = window.require('WAWebMsgKey'); window.Store.OpaqueData = window.require('WAWebMediaOpaqueData'); window.Store.QueryProduct = window.require('WAWebBizProductCatalogBridge'); @@ -77,7 +78,6 @@ exports.ExposeStore = () => { window.Store.PresenceUtils = window.require('WAWebPresenceChatAction'); window.Store.ChatState = window.require('WAWebChatStateBridge'); window.Store.findCommonGroups = window.require('WAWebFindCommonGroupsContactAction').findCommonGroups; - window.Store.StatusUtils = window.require('WAWebContactStatusBridge'); window.Store.ConversationMsgs = window.require('WAWebChatLoadMessages'); window.Store.sendReactionToMsg = window.require('WAWebSendReactionMsgAction').sendReactionToMsg; window.Store.createOrUpdateReactionsModule = window.require('WAWebDBCreateOrUpdateReactions'); @@ -110,7 +110,7 @@ exports.ExposeStore = () => { window.Store.BusinessGatingUtils = window.require('WAWebBizGatingUtils'); window.Store.PollsVotesSchema = window.require('WAWebPollsVotesSchema'); window.Store.PollsSendVote = window.require('WAWebPollsSendVoteMsgAction'); - + window.Store.Settings = { ...window.require('WAWebUserPrefsGeneral'), ...window.require('WAWebUserPrefsNotifications'), @@ -200,6 +200,12 @@ exports.ExposeStore = () => { ...window.require('WAWebSaveContactAction'), ...window.require('WAWebDeleteContactAction') }; + window.Store.StatusUtils = { + ...window.require('WAWebContactStatusBridge'), + ...window.require('WAWebSendStatusMsgAction'), + ...window.require('WAWebRevokeStatusAction'), + ...window.require('WAWebStatusGatingUtils') + }; if (!window.Store.Chat._find || !window.Store.Chat.findImpl) { window.Store.Chat._find = e => { diff --git a/src/util/Injected/Utils.js b/src/util/Injected/Utils.js index b6d08ef..af2a39a 100644 --- a/src/util/Injected/Utils.js +++ b/src/util/Injected/Utils.js @@ -22,10 +22,11 @@ exports.LoadUtils = () => { window.WWebJS.sendMessage = async (chat, content, options = {}) => { const isChannel = window.Store.ChatGetters.getIsNewsletter(chat); + const isStatus = window.Store.ChatGetters.getIsBroadcast(chat); let mediaOptions = {}; if (options.media) { - mediaOptions = options.sendMediaAsSticker && !isChannel + mediaOptions = options.sendMediaAsSticker && !isChannel && !isStatus ? await window.WWebJS.processStickerData(options.media) : await window.WWebJS.processMediaData(options.media, { forceSticker: options.sendMediaAsSticker, @@ -33,7 +34,8 @@ exports.LoadUtils = () => { forceVoice: options.sendAudioAsVoice, forceDocument: options.sendMediaAsDocument, forceMediaHd: options.sendMediaAsHd, - sendToChannel: isChannel + sendToChannel: isChannel, + sendToStatus: isStatus }); mediaOptions.caption = options.caption; content = options.sendMediaAsSticker ? undefined : mediaOptions.preview; @@ -246,6 +248,10 @@ exports.LoadUtils = () => { participant = window.Store.WidFactory.asUserWidOrThrow(from); } + if (typeof chat.id?.isStatus === 'function' && chat.id.isStatus()) { + participant = window.Store.WidFactory.asUserWidOrThrow(from); + } + const newMsgKey = new window.Store.MsgKey({ from: from, to: chat.id, @@ -320,6 +326,37 @@ exports.LoadUtils = () => { return msg; } + if (isStatus) { + const { backgroundColor, fontStyle } = extraOptions; + const isMedia = Object.keys(mediaOptions).length > 0; + const mediaUpdate = data => window.Store.MediaUpdate(data, mediaOptions); + const msg = new window.Store.Msg.modelClass({ + ...message, + author: participant ? participant : null, + messageSecret: window.crypto.getRandomValues(new Uint8Array(32)), + cannotBeRanked: window.Store.StatusUtils.canCheckStatusRankingPosterGating() + }); + + // for text only + const statusOptions = { + color: backgroundColor && window.WWebJS.assertColor(backgroundColor) || 0xff7acca5, + font: fontStyle >= 0 && fontStyle <= 7 && fontStyle || 0, + text: msg.body + }; + + await window.Store.StatusUtils[ + isMedia ? + 'sendStatusMediaMsgAction' : 'sendStatusTextMsgAction' + ]( + ...( + isMedia ? + [msg, mediaUpdate] : [statusOptions] + ) + ); + + return msg; + } + const [msgPromise, sendMsgResultPromise] = window.Store.SendMessage.addAndSendMsgToChat(chat, message); await msgPromise; @@ -407,7 +444,7 @@ exports.LoadUtils = () => { return stickerInfo; }; - window.WWebJS.processMediaData = async (mediaInfo, { forceSticker, forceGif, forceVoice, forceDocument, forceMediaHd, sendToChannel }) => { + window.WWebJS.processMediaData = async (mediaInfo, { forceSticker, forceGif, forceVoice, forceDocument, forceMediaHd, sendToChannel, sendToStatus }) => { const file = window.WWebJS.mediaInfoToFile(mediaInfo); const opaqueData = await window.Store.OpaqueData.createFromData(file, file.type); const mediaParams = { @@ -434,7 +471,7 @@ exports.LoadUtils = () => { throw new Error('media-fault: sendToChat filehash undefined'); } - if (forceVoice && mediaData.type === 'ptt') { + if ((forceVoice && mediaData.type === 'ptt') || (sendToStatus && mediaData.type === 'audio')) { const waveform = mediaObject.contentInfo.waveform; mediaData.waveform = waveform || await window.WWebJS.generateWaveform(file); @@ -463,7 +500,7 @@ exports.LoadUtils = () => { mimetype: mediaData.mimetype, mediaObject, mediaType, - ...(sendToChannel ? { calculateToken: window.Store.SendChannelMessage.getRandomFilehash } : {}) + ...(sendToChannel ? { calculateToken: window.Store.SendChannelMessage.getRandomFilehash() } : {}) }; const uploadedMedia = !sendToChannel @@ -1165,4 +1202,20 @@ exports.LoadUtils = () => { return { lid, phone }; }; + + window.WWebJS.assertColor = (hex) => { + let color; + if (typeof hex === 'number') { + color = hex > 0 ? hex : 0xffffffff + parseInt(hex) + 1; + } else if (typeof hex === 'string') { + let number = hex.trim().replace('#', ''); + if (number.length <= 6) { + number = 'FF' + number.padStart(6, '0'); + } + color = parseInt(number, 16); + } else { + throw 'Invalid hex color'; + } + return color; + }; };