mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
Merge branch 'WhiskeySockets:master' into master
This commit is contained in:
@@ -61,14 +61,14 @@
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"jest": "^29.7.0",
|
||||
"jimp": "^0.16.1",
|
||||
"jimp": "^1.6.0",
|
||||
"json": "^11.0.0",
|
||||
"link-preview-js": "^3.0.0",
|
||||
"open": "^8.4.2",
|
||||
"prettier": "^3.5.3",
|
||||
"protobufjs-cli": "^1.1.3",
|
||||
"release-it": "^15.10.3",
|
||||
"sharp": "^0.32.6",
|
||||
"sharp": "^0.34.2",
|
||||
"ts-jest": "^29.3.2",
|
||||
"ts-node": "^10.8.1",
|
||||
"typedoc": "^0.27.9",
|
||||
@@ -77,9 +77,9 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"audio-decode": "^2.1.3",
|
||||
"jimp": "^0.16.1",
|
||||
"jimp": "^1.6.0",
|
||||
"link-preview-js": "^3.0.0",
|
||||
"sharp": "^0.32.6"
|
||||
"sharp": "^0.34.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"audio-decode": {
|
||||
|
||||
@@ -257,7 +257,11 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
}
|
||||
|
||||
/** update the profile picture for yourself or a group */
|
||||
const updateProfilePicture = async (jid: string, content: WAMediaUpload) => {
|
||||
const updateProfilePicture = async (
|
||||
jid: string,
|
||||
content: WAMediaUpload,
|
||||
dimensions?: { width: number; height: number }
|
||||
) => {
|
||||
let targetJid
|
||||
if (!jid) {
|
||||
throw new Boom(
|
||||
@@ -269,7 +273,7 @@ export const makeChatsSocket = (config: SocketConfig) => {
|
||||
targetJid = jidNormalizedUser(jid) // in case it is someone other than us
|
||||
}
|
||||
|
||||
const { img } = await generateProfilePicture(content)
|
||||
const { img } = await generateProfilePicture(content, dimensions)
|
||||
await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
|
||||
@@ -335,6 +335,7 @@ export const extractGroupMetadata = (result: BinaryNode) => {
|
||||
creation: +group.attrs.creation,
|
||||
owner: group.attrs.creator ? jidNormalizedUser(group.attrs.creator) : undefined,
|
||||
ownerJid: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined,
|
||||
owner_country_code: group.attrs.creator_country_code,
|
||||
desc,
|
||||
descId,
|
||||
descOwner,
|
||||
@@ -359,3 +360,5 @@ export const extractGroupMetadata = (result: BinaryNode) => {
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
export type GroupsSocket = ReturnType<typeof makeGroupsSocket>
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
getBinaryNodeChild,
|
||||
getBinaryNodeChildBuffer,
|
||||
getBinaryNodeChildren,
|
||||
getBinaryNodeChildString,
|
||||
isJidGroup,
|
||||
isJidStatusBroadcast,
|
||||
isJidUser,
|
||||
@@ -403,6 +404,12 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
logger.debug({ jid }, 'got privacy token update')
|
||||
}
|
||||
|
||||
break
|
||||
case 'newsletter':
|
||||
await handleNewsletterNotification(node)
|
||||
break
|
||||
case 'mex':
|
||||
await handleMexNewsletterNotification(node)
|
||||
break
|
||||
case 'w:gp2':
|
||||
handleGroupNotification(node.attrs.participant, child, result)
|
||||
@@ -1083,6 +1090,158 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Handles newsletter notifications
|
||||
async function handleNewsletterNotification(node: BinaryNode) {
|
||||
const from = node.attrs.from
|
||||
const [child] = getAllBinaryNodeChildren(node)
|
||||
const author = node.attrs.participant
|
||||
|
||||
logger.info({ from, child }, 'got newsletter notification')
|
||||
|
||||
switch (child.tag) {
|
||||
case 'reaction':
|
||||
const reactionUpdate = {
|
||||
id: from,
|
||||
server_id: child.attrs.message_id,
|
||||
reaction: {
|
||||
code: getBinaryNodeChildString(child, 'reaction'),
|
||||
count: 1
|
||||
}
|
||||
}
|
||||
ev.emit('newsletter.reaction', reactionUpdate)
|
||||
break
|
||||
|
||||
case 'view':
|
||||
const viewUpdate = {
|
||||
id: from,
|
||||
server_id: child.attrs.message_id,
|
||||
count: parseInt(child.content?.toString() || '0', 10)
|
||||
}
|
||||
ev.emit('newsletter.view', viewUpdate)
|
||||
break
|
||||
|
||||
case 'participant':
|
||||
const participantUpdate = {
|
||||
id: from,
|
||||
author,
|
||||
user: child.attrs.jid,
|
||||
action: child.attrs.action,
|
||||
new_role: child.attrs.role
|
||||
}
|
||||
ev.emit('newsletter-participants.update', participantUpdate)
|
||||
break
|
||||
|
||||
case 'update':
|
||||
const settingsNode = getBinaryNodeChild(child, 'settings')
|
||||
if (settingsNode) {
|
||||
const update: Record<string, any> = {}
|
||||
const nameNode = getBinaryNodeChild(settingsNode, 'name')
|
||||
if (nameNode?.content) update.name = nameNode.content.toString()
|
||||
|
||||
const descriptionNode = getBinaryNodeChild(settingsNode, 'description')
|
||||
if (descriptionNode?.content) update.description = descriptionNode.content.toString()
|
||||
|
||||
ev.emit('newsletter-settings.update', {
|
||||
id: from,
|
||||
update
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'message':
|
||||
const plaintextNode = getBinaryNodeChild(child, 'plaintext')
|
||||
if (plaintextNode?.content) {
|
||||
try {
|
||||
const contentBuf =
|
||||
typeof plaintextNode.content === 'string'
|
||||
? Buffer.from(plaintextNode.content, 'binary')
|
||||
: Buffer.from(plaintextNode.content as Uint8Array)
|
||||
const messageProto = proto.Message.decode(contentBuf)
|
||||
const fullMessage = proto.WebMessageInfo.fromObject({
|
||||
key: {
|
||||
remoteJid: from,
|
||||
id: child.attrs.message_id || child.attrs.server_id,
|
||||
fromMe: false
|
||||
},
|
||||
message: messageProto,
|
||||
messageTimestamp: +child.attrs.t
|
||||
})
|
||||
await upsertMessage(fullMessage, 'append')
|
||||
logger.info('Processed plaintext newsletter message')
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to decode plaintext newsletter message')
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
logger.warn({ node }, 'Unknown newsletter notification')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Handles mex newsletter notifications
|
||||
async function handleMexNewsletterNotification(node: BinaryNode) {
|
||||
const mexNode = getBinaryNodeChild(node, 'mex')
|
||||
if (!mexNode?.content) {
|
||||
logger.warn({ node }, 'Invalid mex newsletter notification')
|
||||
return
|
||||
}
|
||||
|
||||
let data: any
|
||||
try {
|
||||
data = JSON.parse(mexNode.content.toString())
|
||||
} catch (error) {
|
||||
logger.error({ err: error, node }, 'Failed to parse mex newsletter notification')
|
||||
return
|
||||
}
|
||||
|
||||
const operation = data?.operation
|
||||
const updates = data?.updates
|
||||
|
||||
if (!updates || !operation) {
|
||||
logger.warn({ data }, 'Invalid mex newsletter notification content')
|
||||
return
|
||||
}
|
||||
|
||||
logger.info({ operation, updates }, 'got mex newsletter notification')
|
||||
|
||||
switch (operation) {
|
||||
case 'NotificationNewsletterUpdate':
|
||||
for (const update of updates) {
|
||||
if (update.jid && update.settings && Object.keys(update.settings).length > 0) {
|
||||
ev.emit('newsletter-settings.update', {
|
||||
id: update.jid,
|
||||
update: update.settings
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case 'NotificationNewsletterAdminPromote':
|
||||
for (const update of updates) {
|
||||
if (update.jid && update.user) {
|
||||
ev.emit('newsletter-participants.update', {
|
||||
id: update.jid,
|
||||
author: node.attrs.from,
|
||||
user: update.user,
|
||||
new_role: 'ADMIN',
|
||||
action: 'promote'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
default:
|
||||
logger.info({ operation, data }, 'Unhandled mex newsletter notification')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// recv a message
|
||||
ws.on('CB:message', (node: BinaryNode) => {
|
||||
processNode('message', node, 'processing message', handleMessage)
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
assertMediaContent,
|
||||
bindWaitForEvent,
|
||||
decryptMediaRetryData,
|
||||
encodeNewsletterMessage,
|
||||
encodeSignedDeviceIdentity,
|
||||
encodeWAMessage,
|
||||
encryptMediaRetryRequest,
|
||||
@@ -46,6 +47,7 @@ import {
|
||||
} from '../WABinary'
|
||||
import { USyncQuery, USyncUser } from '../WAUSync'
|
||||
import { makeGroupsSocket } from './groups'
|
||||
import { makeNewsletterSocket, NewsletterSocket } from './newsletter'
|
||||
|
||||
export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
const {
|
||||
@@ -56,7 +58,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
patchMessageBeforeSending,
|
||||
cachedGroupMetadata
|
||||
} = config
|
||||
const sock = makeGroupsSocket(config)
|
||||
const sock: NewsletterSocket = makeNewsletterSocket(makeGroupsSocket(config))
|
||||
const {
|
||||
ev,
|
||||
authState,
|
||||
@@ -373,6 +375,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
const isGroup = server === 'g.us'
|
||||
const isStatus = jid === statusJid
|
||||
const isLid = server === 'lid'
|
||||
const isNewsletter = server === 'newsletter'
|
||||
|
||||
msgId = msgId || generateMessageIDV2(sock.user?.id)
|
||||
useUserDevicesCache = useUserDevicesCache !== false
|
||||
@@ -387,7 +390,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
deviceSentMessage: {
|
||||
destinationJid,
|
||||
message
|
||||
}
|
||||
},
|
||||
messageContextInfo: message.messageContextInfo
|
||||
}
|
||||
|
||||
const extraAttrs = {}
|
||||
@@ -410,6 +414,30 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
extraAttrs['mediatype'] = mediaType
|
||||
}
|
||||
|
||||
if (isNewsletter) {
|
||||
// Patch message if needed, then encode as plaintext
|
||||
const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message
|
||||
const bytes = encodeNewsletterMessage(patched as proto.IMessage)
|
||||
binaryNodeContent.push({
|
||||
tag: 'plaintext',
|
||||
attrs: {},
|
||||
content: bytes
|
||||
})
|
||||
const stanza: BinaryNode = {
|
||||
tag: 'message',
|
||||
attrs: {
|
||||
to: jid,
|
||||
id: msgId,
|
||||
type: getMessageType(message),
|
||||
...(additionalAttributes || {})
|
||||
},
|
||||
content: binaryNodeContent
|
||||
}
|
||||
logger.debug({ msgId }, `sending newsletter message to ${jid}`)
|
||||
await sendNode(stanza)
|
||||
return
|
||||
}
|
||||
|
||||
if (normalizeMessageContent(message)?.pinInChatMessage) {
|
||||
extraAttrs['decrypt-fail'] = 'hide'
|
||||
}
|
||||
|
||||
58
src/Socket/mex.ts
Normal file
58
src/Socket/mex.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { BinaryNode } from '../WABinary'
|
||||
import { getBinaryNodeChild, S_WHATSAPP_NET } from '../WABinary'
|
||||
|
||||
const wMexQuery = (
|
||||
variables: Record<string, unknown>,
|
||||
queryId: string,
|
||||
query: (node: BinaryNode) => Promise<BinaryNode>,
|
||||
generateMessageTag: () => string
|
||||
) => {
|
||||
return query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
id: generateMessageTag(),
|
||||
type: 'get',
|
||||
to: S_WHATSAPP_NET,
|
||||
xmlns: 'w:mex'
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'query',
|
||||
attrs: { query_id: queryId },
|
||||
content: Buffer.from(JSON.stringify({ variables }), 'utf-8')
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
export const executeWMexQuery = async <T>(
|
||||
variables: Record<string, unknown>,
|
||||
queryId: string,
|
||||
dataPath: string,
|
||||
query: (node: BinaryNode) => Promise<BinaryNode>,
|
||||
generateMessageTag: () => string
|
||||
): Promise<T> => {
|
||||
const result = await wMexQuery(variables, queryId, query, generateMessageTag)
|
||||
const child = getBinaryNodeChild(result, 'result')
|
||||
if (child?.content) {
|
||||
const data = JSON.parse(child.content.toString())
|
||||
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
const errorMessages = data.errors.map((err: Error) => err.message || 'Unknown error').join(', ')
|
||||
const firstError = data.errors[0]
|
||||
const errorCode = firstError.extensions?.error_code || 400
|
||||
throw new Boom(`GraphQL server error: ${errorMessages}`, { statusCode: errorCode, data: firstError })
|
||||
}
|
||||
|
||||
const response = dataPath ? data?.data?.[dataPath] : data?.data
|
||||
if (typeof response !== 'undefined') {
|
||||
return response as T
|
||||
}
|
||||
}
|
||||
|
||||
const action = (dataPath || '').startsWith('xwa2_')
|
||||
? dataPath.substring(5).replace(/_/g, ' ')
|
||||
: dataPath?.replace(/_/g, ' ')
|
||||
throw new Boom(`Failed to ${action}, unexpected response structure.`, { statusCode: 400, data: result })
|
||||
}
|
||||
227
src/Socket/newsletter.ts
Normal file
227
src/Socket/newsletter.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import type { NewsletterCreateResponse, WAMediaUpload } from '../Types'
|
||||
import { NewsletterMetadata, NewsletterUpdate, QueryIds, XWAPaths } from '../Types'
|
||||
import { generateProfilePicture } from '../Utils/messages-media'
|
||||
import { getBinaryNodeChild } from '../WABinary'
|
||||
import { GroupsSocket } from './groups'
|
||||
import { executeWMexQuery as genericExecuteWMexQuery } from './mex'
|
||||
|
||||
const parseNewsletterCreateResponse = (response: NewsletterCreateResponse): NewsletterMetadata => {
|
||||
const { id, thread_metadata: thread, viewer_metadata: viewer } = response
|
||||
return {
|
||||
id: id,
|
||||
owner: undefined,
|
||||
name: thread.name.text,
|
||||
creation_time: parseInt(thread.creation_time, 10),
|
||||
description: thread.description.text,
|
||||
invite: thread.invite,
|
||||
subscribers: parseInt(thread.subscribers_count, 10),
|
||||
verification: thread.verification,
|
||||
picture: {
|
||||
id: thread.picture.id,
|
||||
directPath: thread.picture.direct_path
|
||||
},
|
||||
mute_state: viewer.mute
|
||||
}
|
||||
}
|
||||
|
||||
const parseNewsletterMetadata = (result: unknown): NewsletterMetadata | null => {
|
||||
if (typeof result !== 'object' || result === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if ('id' in result && typeof result.id === 'string') {
|
||||
return result as NewsletterMetadata
|
||||
}
|
||||
|
||||
if ('result' in result && typeof result.result === 'object' && result.result !== null && 'id' in result.result) {
|
||||
return result.result as NewsletterMetadata
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const makeNewsletterSocket = (sock: GroupsSocket) => {
|
||||
const { query, generateMessageTag } = sock
|
||||
|
||||
const executeWMexQuery = <T>(variables: Record<string, unknown>, queryId: string, dataPath: string): Promise<T> => {
|
||||
return genericExecuteWMexQuery<T>(variables, queryId, dataPath, query, generateMessageTag)
|
||||
}
|
||||
|
||||
const newsletterUpdate = async (jid: string, updates: NewsletterUpdate) => {
|
||||
const variables = {
|
||||
newsletter_id: jid,
|
||||
updates: {
|
||||
...updates,
|
||||
settings: null
|
||||
}
|
||||
}
|
||||
return executeWMexQuery(variables, QueryIds.UPDATE_METADATA, 'xwa2_newsletter_update')
|
||||
}
|
||||
|
||||
return {
|
||||
...sock,
|
||||
newsletterCreate: async (name: string, description?: string): Promise<NewsletterMetadata> => {
|
||||
const variables = {
|
||||
input: {
|
||||
name,
|
||||
description: description ?? null
|
||||
}
|
||||
}
|
||||
const rawResponse = await executeWMexQuery<NewsletterCreateResponse>(
|
||||
variables,
|
||||
QueryIds.CREATE,
|
||||
XWAPaths.xwa2_newsletter_create
|
||||
)
|
||||
return parseNewsletterCreateResponse(rawResponse)
|
||||
},
|
||||
|
||||
newsletterUpdate,
|
||||
|
||||
newsletterSubscribers: async (jid: string) => {
|
||||
return executeWMexQuery<{ subscribers: number }>(
|
||||
{ newsletter_id: jid },
|
||||
QueryIds.SUBSCRIBERS,
|
||||
XWAPaths.xwa2_newsletter_subscribers
|
||||
)
|
||||
},
|
||||
|
||||
newsletterMetadata: async (type: 'invite' | 'jid', key: string) => {
|
||||
const variables = {
|
||||
fetch_creation_time: true,
|
||||
fetch_full_image: true,
|
||||
fetch_viewer_metadata: true,
|
||||
input: {
|
||||
key,
|
||||
type: type.toUpperCase()
|
||||
}
|
||||
}
|
||||
const result = await executeWMexQuery<unknown>(variables, QueryIds.METADATA, XWAPaths.xwa2_newsletter_metadata)
|
||||
return parseNewsletterMetadata(result)
|
||||
},
|
||||
|
||||
newsletterFollow: (jid: string) => {
|
||||
return executeWMexQuery({ newsletter_id: jid }, QueryIds.FOLLOW, XWAPaths.xwa2_newsletter_follow)
|
||||
},
|
||||
|
||||
newsletterUnfollow: (jid: string) => {
|
||||
return executeWMexQuery({ newsletter_id: jid }, QueryIds.UNFOLLOW, XWAPaths.xwa2_newsletter_unfollow)
|
||||
},
|
||||
|
||||
newsletterMute: (jid: string) => {
|
||||
return executeWMexQuery({ newsletter_id: jid }, QueryIds.MUTE, XWAPaths.xwa2_newsletter_mute_v2)
|
||||
},
|
||||
|
||||
newsletterUnmute: (jid: string) => {
|
||||
return executeWMexQuery({ newsletter_id: jid }, QueryIds.UNMUTE, XWAPaths.xwa2_newsletter_unmute_v2)
|
||||
},
|
||||
|
||||
newsletterUpdateName: async (jid: string, name: string) => {
|
||||
return await newsletterUpdate(jid, { name })
|
||||
},
|
||||
|
||||
newsletterUpdateDescription: async (jid: string, description: string) => {
|
||||
return await newsletterUpdate(jid, { description })
|
||||
},
|
||||
|
||||
newsletterUpdatePicture: async (jid: string, content: WAMediaUpload) => {
|
||||
const { img } = await generateProfilePicture(content)
|
||||
return await newsletterUpdate(jid, { picture: img.toString('base64') })
|
||||
},
|
||||
|
||||
newsletterRemovePicture: async (jid: string) => {
|
||||
return await newsletterUpdate(jid, { picture: '' })
|
||||
},
|
||||
|
||||
newsletterReactMessage: async (jid: string, serverId: string, reaction?: string) => {
|
||||
await query({
|
||||
tag: 'message',
|
||||
attrs: {
|
||||
to: jid,
|
||||
...(reaction ? {} : { edit: '7' }),
|
||||
type: 'reaction',
|
||||
server_id: serverId,
|
||||
id: generateMessageTag()
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'reaction',
|
||||
attrs: reaction ? { code: reaction } : {}
|
||||
}
|
||||
]
|
||||
})
|
||||
},
|
||||
|
||||
newsletterFetchMessages: async (jid: string, count: number, since: number, after: number) => {
|
||||
const messageUpdateAttrs: { count: string; since?: string; after?: string } = {
|
||||
count: count.toString()
|
||||
}
|
||||
if (typeof since === 'number') {
|
||||
messageUpdateAttrs.since = since.toString()
|
||||
}
|
||||
|
||||
if (after) {
|
||||
messageUpdateAttrs.after = after.toString()
|
||||
}
|
||||
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
id: generateMessageTag(),
|
||||
type: 'get',
|
||||
xmlns: 'newsletter',
|
||||
to: jid
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'message_updates',
|
||||
attrs: messageUpdateAttrs
|
||||
}
|
||||
]
|
||||
})
|
||||
return result
|
||||
},
|
||||
|
||||
subscribeNewsletterUpdates: async (jid: string): Promise<{ duration: string } | null> => {
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
id: generateMessageTag(),
|
||||
type: 'set',
|
||||
xmlns: 'newsletter',
|
||||
to: jid
|
||||
},
|
||||
content: [{ tag: 'live_updates', attrs: {}, content: [] }]
|
||||
})
|
||||
const liveUpdatesNode = getBinaryNodeChild(result, 'live_updates')
|
||||
const duration = liveUpdatesNode?.attrs?.duration
|
||||
return duration ? { duration: duration } : null
|
||||
},
|
||||
|
||||
newsletterAdminCount: async (jid: string): Promise<number> => {
|
||||
const response = await executeWMexQuery<{ admin_count: number }>(
|
||||
{ newsletter_id: jid },
|
||||
QueryIds.ADMIN_COUNT,
|
||||
XWAPaths.xwa2_newsletter_admin_count
|
||||
)
|
||||
return response.admin_count
|
||||
},
|
||||
|
||||
newsletterChangeOwner: async (jid: string, newOwnerJid: string) => {
|
||||
await executeWMexQuery(
|
||||
{ newsletter_id: jid, user_id: newOwnerJid },
|
||||
QueryIds.CHANGE_OWNER,
|
||||
XWAPaths.xwa2_newsletter_change_owner
|
||||
)
|
||||
},
|
||||
|
||||
newsletterDemote: async (jid: string, userJid: string) => {
|
||||
await executeWMexQuery({ newsletter_id: jid, user_id: userJid }, QueryIds.DEMOTE, XWAPaths.xwa2_newsletter_demote)
|
||||
},
|
||||
|
||||
newsletterDelete: async (jid: string) => {
|
||||
await executeWMexQuery({ newsletter_id: jid }, QueryIds.DELETE, XWAPaths.xwa2_newsletter_delete_v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type NewsletterSocket = ReturnType<typeof makeNewsletterSocket>
|
||||
@@ -345,7 +345,6 @@ export const makeSocket = (config: SocketConfig) => {
|
||||
clearTimeout(qrTimer)
|
||||
|
||||
ws.removeAllListeners('close')
|
||||
ws.removeAllListeners('error')
|
||||
ws.removeAllListeners('open')
|
||||
ws.removeAllListeners('message')
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ export type ChatModification =
|
||||
}
|
||||
| {
|
||||
clear: boolean
|
||||
lastMessages: LastMessageList
|
||||
}
|
||||
| {
|
||||
deleteForMe: { deleteMedia: boolean; key: WAMessageKey; timestamp: number }
|
||||
|
||||
@@ -71,6 +71,16 @@ export type BaileysEventMap = {
|
||||
call: WACallEvent[]
|
||||
'labels.edit': Label
|
||||
'labels.association': { association: LabelAssociation; type: 'add' | 'remove' }
|
||||
|
||||
/** Newsletter-related events */
|
||||
'newsletter.reaction': {
|
||||
id: string
|
||||
server_id: string
|
||||
reaction: { code?: string; count?: number; removed?: boolean }
|
||||
}
|
||||
'newsletter.view': { id: string; server_id: string; count: number }
|
||||
'newsletter-participants.update': { id: string; author: string; user: string; new_role: string; action: string }
|
||||
'newsletter-settings.update': { id: string; update: any }
|
||||
}
|
||||
|
||||
export type BufferedEventData = {
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface GroupMetadata {
|
||||
addressingMode: 'pn' | 'lid'
|
||||
owner: string | undefined
|
||||
ownerJid?: string | undefined
|
||||
owner_country_code: string
|
||||
subject: string
|
||||
/** group subject owner */
|
||||
subjectOwner?: string
|
||||
|
||||
@@ -9,15 +9,17 @@ import { CacheStore } from './Socket'
|
||||
|
||||
// export the WAMessage Prototypes
|
||||
export { proto as WAProto }
|
||||
export type WAMessage = proto.IWebMessageInfo
|
||||
export type WAMessage = proto.IWebMessageInfo & { key: WAMessageKey }
|
||||
export type WAMessageContent = proto.IMessage
|
||||
export type WAContactMessage = proto.Message.IContactMessage
|
||||
export type WAContactsArrayMessage = proto.Message.IContactsArrayMessage
|
||||
export type WAMessageKey = proto.IMessageKey & {
|
||||
senderLid?: string
|
||||
server_id?: string
|
||||
senderPn?: string
|
||||
participantLid?: string
|
||||
participantPn?: string
|
||||
isViewOnce?: boolean
|
||||
}
|
||||
export type WATextMessage = proto.Message.IExtendedTextMessage
|
||||
export type WAContextInfo = proto.IContextInfo
|
||||
@@ -292,6 +294,7 @@ export type MediaGenerationOptions = {
|
||||
export type MessageContentGenerationOptions = MediaGenerationOptions & {
|
||||
getUrlInfo?: (text: string) => Promise<WAUrlInfo | undefined>
|
||||
getProfilePicUrl?: (jid: string, type: 'image' | 'preview') => Promise<string | undefined>
|
||||
jid?: string
|
||||
}
|
||||
export type MessageGenerationOptions = MessageContentGenerationOptions & MessageGenerationOptionsFromContent
|
||||
|
||||
|
||||
98
src/Types/Newsletter.ts
Normal file
98
src/Types/Newsletter.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
export enum XWAPaths {
|
||||
xwa2_newsletter_create = 'xwa2_newsletter_create',
|
||||
xwa2_newsletter_subscribers = 'xwa2_newsletter_subscribers',
|
||||
xwa2_newsletter_view = 'xwa2_newsletter_view',
|
||||
xwa2_newsletter_metadata = 'xwa2_newsletter',
|
||||
xwa2_newsletter_admin_count = 'xwa2_newsletter_admin',
|
||||
xwa2_newsletter_mute_v2 = 'xwa2_newsletter_mute_v2',
|
||||
xwa2_newsletter_unmute_v2 = 'xwa2_newsletter_unmute_v2',
|
||||
xwa2_newsletter_follow = 'xwa2_newsletter_follow',
|
||||
xwa2_newsletter_unfollow = 'xwa2_newsletter_unfollow',
|
||||
xwa2_newsletter_change_owner = 'xwa2_newsletter_change_owner',
|
||||
xwa2_newsletter_demote = 'xwa2_newsletter_demote',
|
||||
xwa2_newsletter_delete_v2 = 'xwa2_newsletter_delete_v2'
|
||||
}
|
||||
export enum QueryIds {
|
||||
CREATE = '8823471724422422',
|
||||
UPDATE_METADATA = '24250201037901610',
|
||||
METADATA = '6563316087068696',
|
||||
SUBSCRIBERS = '9783111038412085',
|
||||
FOLLOW = '7871414976211147',
|
||||
UNFOLLOW = '7238632346214362',
|
||||
MUTE = '29766401636284406',
|
||||
UNMUTE = '9864994326891137',
|
||||
ADMIN_COUNT = '7130823597031706',
|
||||
CHANGE_OWNER = '7341777602580933',
|
||||
DEMOTE = '6551828931592903',
|
||||
DELETE = '30062808666639665'
|
||||
}
|
||||
export type NewsletterUpdate = {
|
||||
name?: string
|
||||
description?: string
|
||||
picture?: string
|
||||
}
|
||||
export interface NewsletterCreateResponse {
|
||||
id: string
|
||||
state: { type: string }
|
||||
thread_metadata: {
|
||||
creation_time: string
|
||||
description: { id: string; text: string; update_time: string }
|
||||
handle: string | null
|
||||
invite: string
|
||||
name: { id: string; text: string; update_time: string }
|
||||
picture: { direct_path: string; id: string; type: string }
|
||||
preview: { direct_path: string; id: string; type: string }
|
||||
subscribers_count: string
|
||||
verification: 'VERIFIED' | 'UNVERIFIED'
|
||||
}
|
||||
viewer_metadata: {
|
||||
mute: 'ON' | 'OFF'
|
||||
role: NewsletterViewRole
|
||||
}
|
||||
}
|
||||
export interface NewsletterCreateResponse {
|
||||
id: string
|
||||
state: { type: string }
|
||||
thread_metadata: {
|
||||
creation_time: string
|
||||
description: { id: string; text: string; update_time: string }
|
||||
handle: string | null
|
||||
invite: string
|
||||
name: { id: string; text: string; update_time: string }
|
||||
picture: { direct_path: string; id: string; type: string }
|
||||
preview: { direct_path: string; id: string; type: string }
|
||||
subscribers_count: string
|
||||
verification: 'VERIFIED' | 'UNVERIFIED'
|
||||
}
|
||||
viewer_metadata: {
|
||||
mute: 'ON' | 'OFF'
|
||||
role: NewsletterViewRole
|
||||
}
|
||||
}
|
||||
export type NewsletterViewRole = 'ADMIN' | 'GUEST' | 'OWNER' | 'SUBSCRIBER'
|
||||
export interface NewsletterMetadata {
|
||||
id: string
|
||||
owner?: string
|
||||
name: string
|
||||
description?: string
|
||||
invite?: string
|
||||
creation_time?: number
|
||||
subscribers?: number
|
||||
picture?: {
|
||||
url?: string
|
||||
directPath?: string
|
||||
mediaKey?: string
|
||||
id?: string
|
||||
}
|
||||
verification?: 'VERIFIED' | 'UNVERIFIED'
|
||||
reaction_codes?: {
|
||||
code: string
|
||||
count: number
|
||||
}[]
|
||||
mute_state?: 'ON' | 'OFF'
|
||||
thread_metadata?: {
|
||||
creation_time?: number
|
||||
name?: string
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export * from './Events'
|
||||
export * from './Product'
|
||||
export * from './Call'
|
||||
export * from './Signal'
|
||||
export * from './Newsletter'
|
||||
|
||||
import { AuthenticationState } from './Auth'
|
||||
import { SocketConfig } from './Socket'
|
||||
|
||||
@@ -584,7 +584,9 @@ export const chatModificationToAppPatch = (mod: ChatModification, jid: string) =
|
||||
} else if ('clear' in mod) {
|
||||
patch = {
|
||||
syncAction: {
|
||||
clearChatAction: {} // add message range later
|
||||
clearChatAction: {
|
||||
messageRange: getMessageRange(mod.lastMessages)
|
||||
}
|
||||
},
|
||||
index: ['clearChat', jid, '1' /*the option here is 0 when keep starred messages is enabled*/, '0'],
|
||||
type: 'regular_high',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { proto } from '../../WAProto'
|
||||
import { SignalRepository, WAMessageKey } from '../Types'
|
||||
import { SignalRepository, WAMessage, WAMessageKey } from '../Types'
|
||||
import {
|
||||
areJidsSameUser,
|
||||
BinaryNode,
|
||||
@@ -114,10 +114,11 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin
|
||||
senderPn: stanza?.attrs?.sender_pn,
|
||||
participant,
|
||||
participantPn: stanza?.attrs?.participant_pn,
|
||||
participantLid: stanza?.attrs?.participant_lid
|
||||
participantLid: stanza?.attrs?.participant_lid,
|
||||
...(msgType === 'newsletter' && stanza.attrs.server_id ? { server_id: stanza.attrs.server_id } : {})
|
||||
}
|
||||
|
||||
const fullMessage: proto.IWebMessageInfo = {
|
||||
const fullMessage: WAMessage = {
|
||||
key,
|
||||
messageTimestamp: +stanza.attrs.t,
|
||||
pushName: pushname,
|
||||
@@ -157,6 +158,10 @@ export const decryptMessageNode = (
|
||||
fullMessage.verifiedBizName = details.verifiedName
|
||||
}
|
||||
|
||||
if (tag === 'unavailable' && attrs.type === 'view_once') {
|
||||
fullMessage.key.isViewOnce = true
|
||||
}
|
||||
|
||||
if (tag !== 'enc' && tag !== 'plaintext') {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -443,3 +443,7 @@ export function bytesToCrockford(buffer: Buffer): string {
|
||||
|
||||
return crockford.join('')
|
||||
}
|
||||
|
||||
export function encodeNewsletterMessage(message: proto.IMessage): Uint8Array {
|
||||
return proto.Message.encode(message).finish()
|
||||
}
|
||||
|
||||
@@ -46,9 +46,6 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
|
||||
|
||||
const msgs = chat.messages || []
|
||||
delete chat.messages
|
||||
delete chat.archived
|
||||
delete chat.muteEndTime
|
||||
delete chat.pinned
|
||||
|
||||
for (const item of msgs) {
|
||||
const message = item.message!
|
||||
@@ -75,10 +72,6 @@ export const processHistoryMessage = (item: proto.IHistorySync) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (isJidUser(chat.id) && chat.readOnly && chat.archived) {
|
||||
delete chat.readOnly
|
||||
}
|
||||
|
||||
chats.push({ ...chat })
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { exec } from 'child_process'
|
||||
import * as Crypto from 'crypto'
|
||||
import { once } from 'events'
|
||||
import { createReadStream, createWriteStream, promises as fs, WriteStream } from 'fs'
|
||||
import { ResizeStrategy } from 'jimp'
|
||||
import type { IAudioMetadata } from 'music-metadata'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
@@ -32,22 +33,12 @@ import { ILogger } from './logger'
|
||||
const getTmpFilesDirectory = () => tmpdir()
|
||||
|
||||
const getImageProcessingLibrary = async () => {
|
||||
const [_jimp, sharp] = await Promise.all([
|
||||
(async () => {
|
||||
const jimp = await import('jimp').catch(() => {})
|
||||
return jimp
|
||||
})(),
|
||||
(async () => {
|
||||
const sharp = await import('sharp').catch(() => {})
|
||||
return sharp
|
||||
})()
|
||||
])
|
||||
const [jimp, sharp] = await Promise.all([import('jimp').catch(() => {}), import('sharp').catch(() => {})])
|
||||
|
||||
if (sharp) {
|
||||
return { sharp }
|
||||
}
|
||||
|
||||
const jimp = _jimp?.default || _jimp
|
||||
if (jimp) {
|
||||
return { jimp }
|
||||
}
|
||||
@@ -60,6 +51,47 @@ export const hkdfInfoKey = (type: MediaType) => {
|
||||
return `WhatsApp ${hkdfInfo} Keys`
|
||||
}
|
||||
|
||||
export const getRawMediaUploadData = async (media: WAMediaUpload, mediaType: MediaType, logger?: ILogger) => {
|
||||
const { stream } = await getStream(media)
|
||||
logger?.debug('got stream for raw upload')
|
||||
|
||||
const hasher = Crypto.createHash('sha256')
|
||||
const filePath = join(tmpdir(), mediaType + generateMessageIDV2())
|
||||
const fileWriteStream = createWriteStream(filePath)
|
||||
|
||||
let fileLength = 0
|
||||
try {
|
||||
for await (const data of stream) {
|
||||
fileLength += data.length
|
||||
hasher.update(data)
|
||||
if (!fileWriteStream.write(data)) {
|
||||
await once(fileWriteStream, 'drain')
|
||||
}
|
||||
}
|
||||
|
||||
fileWriteStream.end()
|
||||
await once(fileWriteStream, 'finish')
|
||||
stream.destroy()
|
||||
const fileSha256 = hasher.digest()
|
||||
logger?.debug('hashed data for raw upload')
|
||||
return {
|
||||
filePath: filePath,
|
||||
fileSha256,
|
||||
fileLength
|
||||
}
|
||||
} catch (error) {
|
||||
fileWriteStream.destroy()
|
||||
stream.destroy()
|
||||
try {
|
||||
await fs.unlink(filePath)
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
||||
export async function getMediaKeys(
|
||||
buffer: Uint8Array | string | null | undefined,
|
||||
@@ -118,15 +150,15 @@ export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | st
|
||||
height: dimensions.height
|
||||
}
|
||||
}
|
||||
} else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
|
||||
const { read, MIME_JPEG, RESIZE_BILINEAR, AUTO } = lib.jimp
|
||||
|
||||
const jimp = await read(bufferOrFilePath as string)
|
||||
} else if ('jimp' in lib && typeof lib.jimp?.Jimp === 'object') {
|
||||
const jimp = await lib.jimp.default.Jimp.read(bufferOrFilePath)
|
||||
const dimensions = {
|
||||
width: jimp.getWidth(),
|
||||
height: jimp.getHeight()
|
||||
width: jimp.width,
|
||||
height: jimp.height
|
||||
}
|
||||
const buffer = await jimp.quality(50).resize(width, AUTO, RESIZE_BILINEAR).getBufferAsync(MIME_JPEG)
|
||||
const buffer = await jimp
|
||||
.resize({ w: width, mode: ResizeStrategy.BILINEAR })
|
||||
.getBuffer('image/jpeg', { quality: 50 })
|
||||
return {
|
||||
buffer,
|
||||
original: dimensions
|
||||
@@ -139,33 +171,39 @@ export const extractImageThumb = async (bufferOrFilePath: Readable | Buffer | st
|
||||
export const encodeBase64EncodedStringForUpload = (b64: string) =>
|
||||
encodeURIComponent(b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''))
|
||||
|
||||
export const generateProfilePicture = async (mediaUpload: WAMediaUpload) => {
|
||||
let bufferOrFilePath: Buffer | string
|
||||
export const generateProfilePicture = async (
|
||||
mediaUpload: WAMediaUpload,
|
||||
dimensions?: { width: number; height: number }
|
||||
) => {
|
||||
let buffer: Buffer
|
||||
|
||||
const { width: w = 640, height: h = 640 } = dimensions || {}
|
||||
|
||||
if (Buffer.isBuffer(mediaUpload)) {
|
||||
bufferOrFilePath = mediaUpload
|
||||
} else if ('url' in mediaUpload) {
|
||||
bufferOrFilePath = mediaUpload.url.toString()
|
||||
buffer = mediaUpload
|
||||
} else {
|
||||
bufferOrFilePath = await toBuffer(mediaUpload.stream)
|
||||
// Use getStream to handle all WAMediaUpload types (Buffer, Stream, URL)
|
||||
const { stream } = await getStream(mediaUpload)
|
||||
// Convert the resulting stream to a buffer
|
||||
buffer = await toBuffer(stream)
|
||||
}
|
||||
|
||||
const lib = await getImageProcessingLibrary()
|
||||
let img: Promise<Buffer>
|
||||
if ('sharp' in lib && typeof lib.sharp?.default === 'function') {
|
||||
img = lib.sharp
|
||||
.default(bufferOrFilePath)
|
||||
.resize(640, 640)
|
||||
.default(buffer)
|
||||
.resize(w, h)
|
||||
.jpeg({
|
||||
quality: 50
|
||||
})
|
||||
.toBuffer()
|
||||
} else if ('jimp' in lib && typeof lib.jimp?.read === 'function') {
|
||||
const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp
|
||||
const jimp = await read(bufferOrFilePath as string)
|
||||
const min = Math.min(jimp.getWidth(), jimp.getHeight())
|
||||
const cropped = jimp.crop(0, 0, min, min)
|
||||
} else if ('jimp' in lib && typeof lib.jimp?.Jimp === 'object') {
|
||||
const jimp = await lib.jimp.default.Jimp.read(buffer)
|
||||
const min = Math.min(jimp.width, jimp.height)
|
||||
const cropped = jimp.crop({ x: 0, y: 0, w: min, h: min })
|
||||
|
||||
img = cropped.quality(50).resize(640, 640, RESIZE_BILINEAR).getBufferAsync(MIME_JPEG)
|
||||
img = cropped.resize({ w, h, mode: ResizeStrategy.BILINEAR }).getBuffer('image/jpeg', { quality: 50 })
|
||||
} else {
|
||||
throw new Boom('No image processing library available')
|
||||
}
|
||||
@@ -269,7 +307,14 @@ export const getStream = async (item: WAMediaUpload, opts?: AxiosRequestConfig)
|
||||
return { stream: item.stream, type: 'readable' } as const
|
||||
}
|
||||
|
||||
if (item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
|
||||
const urlStr = item.url.toString()
|
||||
|
||||
if (urlStr.startsWith('data:')) {
|
||||
const buffer = Buffer.from(urlStr.split(',')[1], 'base64')
|
||||
return { stream: toReadable(buffer), type: 'buffer' } as const
|
||||
}
|
||||
|
||||
if (urlStr.startsWith('http://') || urlStr.startsWith('https://')) {
|
||||
return { stream: await getHttpStream(item.url, opts), type: 'remote' } as const
|
||||
}
|
||||
|
||||
@@ -448,7 +493,12 @@ export const downloadContentFromMessage = async (
|
||||
type: MediaType,
|
||||
opts: MediaDownloadOptions = {}
|
||||
) => {
|
||||
const downloadUrl = url || getUrlFromDirectPath(directPath!)
|
||||
const isValidMediaUrl = url?.startsWith('https://mmg.whatsapp.net/')
|
||||
const downloadUrl = isValidMediaUrl ? url : getUrlFromDirectPath(directPath!)
|
||||
if (!downloadUrl) {
|
||||
throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 })
|
||||
}
|
||||
|
||||
const keys = await getMediaKeys(mediaKey, type)
|
||||
|
||||
return downloadEncryptedContent(downloadUrl, keys, opts)
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
AnyMediaMessageContent,
|
||||
AnyMessageContent,
|
||||
DownloadableMessage,
|
||||
MediaGenerationOptions,
|
||||
MediaType,
|
||||
MessageContentGenerationOptions,
|
||||
MessageGenerationOptions,
|
||||
@@ -23,7 +22,7 @@ import {
|
||||
WAProto,
|
||||
WATextMessage
|
||||
} from '../Types'
|
||||
import { isJidGroup, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary'
|
||||
import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary'
|
||||
import { sha256 } from './crypto'
|
||||
import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics'
|
||||
import { ILogger } from './logger'
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
generateThumbnail,
|
||||
getAudioDuration,
|
||||
getAudioWaveform,
|
||||
getRawMediaUploadData,
|
||||
MediaDownloadOptions
|
||||
} from './messages-media'
|
||||
|
||||
@@ -108,7 +108,10 @@ const assertColor = async color => {
|
||||
}
|
||||
}
|
||||
|
||||
export const prepareWAMessageMedia = async (message: AnyMediaMessageContent, options: MediaGenerationOptions) => {
|
||||
export const prepareWAMessageMedia = async (
|
||||
message: AnyMediaMessageContent,
|
||||
options: MessageContentGenerationOptions
|
||||
) => {
|
||||
const logger = options.logger
|
||||
|
||||
let mediaType: (typeof MEDIA_KEYS)[number] | undefined
|
||||
@@ -127,13 +130,12 @@ export const prepareWAMessageMedia = async (message: AnyMediaMessageContent, opt
|
||||
media: message[mediaType]
|
||||
}
|
||||
delete uploadData[mediaType]
|
||||
// check if cacheable + generate cache key
|
||||
|
||||
const cacheableKey =
|
||||
typeof uploadData.media === 'object' &&
|
||||
'url' in uploadData.media &&
|
||||
!!uploadData.media.url &&
|
||||
!!options.mediaCache &&
|
||||
// generate the key
|
||||
mediaType + ':' + uploadData.media.url.toString()
|
||||
|
||||
if (mediaType === 'document' && !uploadData.fileName) {
|
||||
@@ -144,7 +146,6 @@ export const prepareWAMessageMedia = async (message: AnyMediaMessageContent, opt
|
||||
uploadData.mimetype = MIMETYPE_MAP[mediaType]
|
||||
}
|
||||
|
||||
// check for cache hit
|
||||
if (cacheableKey) {
|
||||
const mediaBuff = options.mediaCache!.get<Buffer>(cacheableKey)
|
||||
if (mediaBuff) {
|
||||
@@ -159,6 +160,48 @@ export const prepareWAMessageMedia = async (message: AnyMediaMessageContent, opt
|
||||
}
|
||||
}
|
||||
|
||||
const isNewsletter = !!options.jid && isJidNewsletter(options.jid)
|
||||
if (isNewsletter) {
|
||||
logger?.info({ key: cacheableKey }, 'Preparing raw media for newsletter')
|
||||
const { filePath, fileSha256, fileLength } = await getRawMediaUploadData(
|
||||
uploadData.media,
|
||||
options.mediaTypeOverride || mediaType,
|
||||
logger
|
||||
)
|
||||
|
||||
const fileSha256B64 = fileSha256.toString('base64')
|
||||
const { mediaUrl, directPath } = await options.upload(filePath, {
|
||||
fileEncSha256B64: fileSha256B64,
|
||||
mediaType: mediaType,
|
||||
timeoutMs: options.mediaUploadTimeoutMs
|
||||
})
|
||||
|
||||
await fs.unlink(filePath)
|
||||
|
||||
const obj = WAProto.Message.fromObject({
|
||||
[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
|
||||
url: mediaUrl,
|
||||
directPath,
|
||||
fileSha256,
|
||||
fileLength,
|
||||
...uploadData,
|
||||
media: undefined
|
||||
})
|
||||
})
|
||||
|
||||
if (uploadData.ptv) {
|
||||
obj.ptvMessage = obj.videoMessage
|
||||
delete obj.videoMessage
|
||||
}
|
||||
|
||||
if (cacheableKey) {
|
||||
logger?.debug({ cacheableKey }, 'set cache')
|
||||
options.mediaCache!.set(cacheableKey, WAProto.Message.encode(obj).finish())
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
|
||||
const requiresThumbnailComputation =
|
||||
(mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined'
|
||||
@@ -174,7 +217,7 @@ export const prepareWAMessageMedia = async (message: AnyMediaMessageContent, opt
|
||||
opts: options.options
|
||||
}
|
||||
)
|
||||
// url safe Base64 encode the SHA256 hash of the body
|
||||
|
||||
const fileEncSha256B64 = fileEncSha256.toString('base64')
|
||||
const [{ mediaUrl, directPath }] = await Promise.all([
|
||||
(async () => {
|
||||
@@ -475,11 +518,11 @@ export const generateWAMessageContent = async (
|
||||
// poll v2 is for community announcement groups (single select and multiple)
|
||||
m.pollCreationMessageV2 = pollCreationMessage
|
||||
} else {
|
||||
if (message.poll.selectableCount > 0) {
|
||||
if (message.poll.selectableCount === 1) {
|
||||
//poll v3 is for single select polls
|
||||
m.pollCreationMessageV3 = pollCreationMessage
|
||||
} else {
|
||||
// poll v3 for multiple choice polls
|
||||
// poll for multiple choice polls
|
||||
m.pollCreationMessage = pollCreationMessage
|
||||
}
|
||||
}
|
||||
@@ -539,7 +582,7 @@ export const generateWAMessageFromContent = (
|
||||
const timestamp = unixTimestampSeconds(options.timestamp)
|
||||
const { quoted, userJid } = options
|
||||
|
||||
if (quoted) {
|
||||
if (quoted && !isJidNewsletter(jid)) {
|
||||
const participant = quoted.key.fromMe
|
||||
? userJid
|
||||
: quoted.participant || quoted.key.participant || quoted.key.remoteJid
|
||||
@@ -574,7 +617,9 @@ export const generateWAMessageFromContent = (
|
||||
// and it's not a protocol message -- delete, toggle disappear message
|
||||
key !== 'protocolMessage' &&
|
||||
// already not converted to disappearing message
|
||||
key !== 'ephemeralMessage'
|
||||
key !== 'ephemeralMessage' &&
|
||||
// newsletters don't support ephemeral messages
|
||||
!isJidNewsletter(jid)
|
||||
) {
|
||||
innerMessage[key].contextInfo = {
|
||||
...(innerMessage[key].contextInfo || {}),
|
||||
@@ -603,7 +648,8 @@ export const generateWAMessageFromContent = (
|
||||
export const generateWAMessage = async (jid: string, content: AnyMessageContent, options: MessageGenerationOptions) => {
|
||||
// ensure msg ID is with every log
|
||||
options.logger = options?.logger?.child({ msgId: options.messageId })
|
||||
return generateWAMessageFromContent(jid, await generateWAMessageContent(content, options), options)
|
||||
// Pass jid in the options to generateWAMessageContent
|
||||
return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options)
|
||||
}
|
||||
|
||||
/** Get the key to access the true type of content */
|
||||
|
||||
@@ -21,7 +21,7 @@ export class USyncDisappearingModeProtocol implements USyncQueryProtocol {
|
||||
}
|
||||
|
||||
parser(node: BinaryNode): DisappearingModeData | undefined {
|
||||
if (node.tag === 'status') {
|
||||
if (node.tag === 'disappearing_mode') {
|
||||
assertNodeErrorFree(node)
|
||||
const duration: number = +node?.attrs.duration
|
||||
const setAt = new Date(+(node?.attrs.t || 0) * 1000)
|
||||
|
||||
Reference in New Issue
Block a user