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 <rajeh@reforward.dev>
This commit is contained in:
RendyCraft047 2025-12-13 11:34:55 +07:00 • committed by GitHub
commit 6aa1bd85ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 152 additions and 12 deletions

View file

@ -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'
});
}
});

10
index.d.ts vendored
View file

@ -113,9 +113,15 @@ declare namespace WAWebJS {
/** Get all current Labels */
getLabels(): Promise<Label[]>
/** Get all current Broadcasts */
getBroadcasts(): Promise<Broadcast[]>
/** Get broadcast instance by current user ID */
getBroadcastById(contactId: string): Promise<Broadcast>
/** Revoke current own status messages */
revokeStatusMessage(messageId: string): Promise<void>
/** Change labels in chats */
addOrRemoveLabels(labelIds: Array<number|string>, chatIds: Array<string>): Promise<void>
@ -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<ChatId[]>
/** Gets the Contact's current status broadcast. */
getBroadcast: () => Promise<Broadcast>
}
export interface ContactId {

View file

@ -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<Message>}
*/
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<Array<Broadcast>>}
@ -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<Broadcast>}
*/
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<void>}
*/
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

View file

@ -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<Broadcast>}
*/
async getBroadcast() {
return await this.client.getBroadcastById(this.id._serialized);
}
}
module.exports = Contact;

View file

@ -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 => {

View file

@ -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;
};
};