Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3f4e83a8af | |||
| d870ffb2a3 | |||
| f6c72530a2 | |||
| a4e7422487 | |||
| c6511d3034 | |||
| d2db4cec97 | |||
| be44c5fdb0 | |||
| f63f0a766f | |||
| fbefa6a0ad | |||
| 9c4cdc3e02 | |||
| ba1c1b0fdb | |||
| 876af2e96c |
@@ -1,7 +1,7 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
package proto;
|
package proto;
|
||||||
|
|
||||||
/// WhatsApp Version: 2.3000.1035484955
|
/// WhatsApp Version: 2.3000.1035577069
|
||||||
|
|
||||||
message ADVDeviceIdentity {
|
message ADVDeviceIdentity {
|
||||||
optional uint32 rawId = 1;
|
optional uint32 rawId = 1;
|
||||||
@@ -2149,7 +2149,6 @@ message LimitSharing {
|
|||||||
CHAT_SETTING = 1;
|
CHAT_SETTING = 1;
|
||||||
BIZ_SUPPORTS_FB_HOSTING = 2;
|
BIZ_SUPPORTS_FB_HOSTING = 2;
|
||||||
UNKNOWN_GROUP = 3;
|
UNKNOWN_GROUP = 3;
|
||||||
DEPRECATION = 4;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4893,6 +4892,7 @@ message SyncActionValue {
|
|||||||
repeated SyncActionValue.BroadcastListParticipant participants = 2;
|
repeated SyncActionValue.BroadcastListParticipant participants = 2;
|
||||||
optional string listName = 3;
|
optional string listName = 3;
|
||||||
repeated string labelIds = 4;
|
repeated string labelIds = 4;
|
||||||
|
optional string audienceExpression = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CallLogAction {
|
message CallLogAction {
|
||||||
|
|||||||
Vendored
+3
-2
@@ -5356,8 +5356,7 @@ export namespace proto {
|
|||||||
UNKNOWN = 0,
|
UNKNOWN = 0,
|
||||||
CHAT_SETTING = 1,
|
CHAT_SETTING = 1,
|
||||||
BIZ_SUPPORTS_FB_HOSTING = 2,
|
BIZ_SUPPORTS_FB_HOSTING = 2,
|
||||||
UNKNOWN_GROUP = 3,
|
UNKNOWN_GROUP = 3
|
||||||
DEPRECATION = 4
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12483,6 +12482,7 @@ export namespace proto {
|
|||||||
participants?: (proto.SyncActionValue.IBroadcastListParticipant[]|null);
|
participants?: (proto.SyncActionValue.IBroadcastListParticipant[]|null);
|
||||||
listName?: (string|null);
|
listName?: (string|null);
|
||||||
labelIds?: (string[]|null);
|
labelIds?: (string[]|null);
|
||||||
|
audienceExpression?: (string|null);
|
||||||
}
|
}
|
||||||
|
|
||||||
class BusinessBroadcastListAction implements IBusinessBroadcastListAction {
|
class BusinessBroadcastListAction implements IBusinessBroadcastListAction {
|
||||||
@@ -12491,6 +12491,7 @@ export namespace proto {
|
|||||||
public participants: proto.SyncActionValue.IBroadcastListParticipant[];
|
public participants: proto.SyncActionValue.IBroadcastListParticipant[];
|
||||||
public listName?: (string|null);
|
public listName?: (string|null);
|
||||||
public labelIds: string[];
|
public labelIds: string[];
|
||||||
|
public audienceExpression?: (string|null);
|
||||||
public static create(properties?: proto.SyncActionValue.IBusinessBroadcastListAction): proto.SyncActionValue.BusinessBroadcastListAction;
|
public static create(properties?: proto.SyncActionValue.IBusinessBroadcastListAction): proto.SyncActionValue.BusinessBroadcastListAction;
|
||||||
public static encode(m: proto.SyncActionValue.IBusinessBroadcastListAction, w?: $protobuf.Writer): $protobuf.Writer;
|
public static encode(m: proto.SyncActionValue.IBusinessBroadcastListAction, w?: $protobuf.Writer): $protobuf.Writer;
|
||||||
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.BusinessBroadcastListAction;
|
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.BusinessBroadcastListAction;
|
||||||
|
|||||||
+21
-9
@@ -26692,10 +26692,6 @@ export const proto = $root.proto = (() => {
|
|||||||
case 3:
|
case 3:
|
||||||
m.limitSharingTrigger = 3;
|
m.limitSharingTrigger = 3;
|
||||||
break;
|
break;
|
||||||
case "DEPRECATION":
|
|
||||||
case 4:
|
|
||||||
m.limitSharingTrigger = 4;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if (d.limitSharingInitiatedByMe != null) {
|
if (d.limitSharingInitiatedByMe != null) {
|
||||||
m.limitSharingInitiatedByMe = Boolean(d.limitSharingInitiatedByMe);
|
m.limitSharingInitiatedByMe = Boolean(d.limitSharingInitiatedByMe);
|
||||||
@@ -36778,10 +36774,6 @@ export const proto = $root.proto = (() => {
|
|||||||
case 3:
|
case 3:
|
||||||
m.trigger = 3;
|
m.trigger = 3;
|
||||||
break;
|
break;
|
||||||
case "DEPRECATION":
|
|
||||||
case 4:
|
|
||||||
m.trigger = 4;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
if (d.limitSharingSettingTimestamp != null) {
|
if (d.limitSharingSettingTimestamp != null) {
|
||||||
if ($util.Long)
|
if ($util.Long)
|
||||||
@@ -36846,7 +36838,6 @@ export const proto = $root.proto = (() => {
|
|||||||
values[valuesById[1] = "CHAT_SETTING"] = 1;
|
values[valuesById[1] = "CHAT_SETTING"] = 1;
|
||||||
values[valuesById[2] = "BIZ_SUPPORTS_FB_HOSTING"] = 2;
|
values[valuesById[2] = "BIZ_SUPPORTS_FB_HOSTING"] = 2;
|
||||||
values[valuesById[3] = "UNKNOWN_GROUP"] = 3;
|
values[valuesById[3] = "UNKNOWN_GROUP"] = 3;
|
||||||
values[valuesById[4] = "DEPRECATION"] = 4;
|
|
||||||
return values;
|
return values;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -87581,6 +87572,7 @@ export const proto = $root.proto = (() => {
|
|||||||
BusinessBroadcastListAction.prototype.participants = $util.emptyArray;
|
BusinessBroadcastListAction.prototype.participants = $util.emptyArray;
|
||||||
BusinessBroadcastListAction.prototype.listName = null;
|
BusinessBroadcastListAction.prototype.listName = null;
|
||||||
BusinessBroadcastListAction.prototype.labelIds = $util.emptyArray;
|
BusinessBroadcastListAction.prototype.labelIds = $util.emptyArray;
|
||||||
|
BusinessBroadcastListAction.prototype.audienceExpression = null;
|
||||||
|
|
||||||
let $oneOfFields;
|
let $oneOfFields;
|
||||||
|
|
||||||
@@ -87596,6 +87588,12 @@ export const proto = $root.proto = (() => {
|
|||||||
set: $util.oneOfSetter($oneOfFields)
|
set: $util.oneOfSetter($oneOfFields)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Virtual OneOf for proto3 optional field
|
||||||
|
Object.defineProperty(BusinessBroadcastListAction.prototype, "_audienceExpression", {
|
||||||
|
get: $util.oneOfGetter($oneOfFields = ["audienceExpression"]),
|
||||||
|
set: $util.oneOfSetter($oneOfFields)
|
||||||
|
});
|
||||||
|
|
||||||
BusinessBroadcastListAction.create = function create(properties) {
|
BusinessBroadcastListAction.create = function create(properties) {
|
||||||
return new BusinessBroadcastListAction(properties);
|
return new BusinessBroadcastListAction(properties);
|
||||||
};
|
};
|
||||||
@@ -87615,6 +87613,8 @@ export const proto = $root.proto = (() => {
|
|||||||
for (var i = 0; i < m.labelIds.length; ++i)
|
for (var i = 0; i < m.labelIds.length; ++i)
|
||||||
w.uint32(34).string(m.labelIds[i]);
|
w.uint32(34).string(m.labelIds[i]);
|
||||||
}
|
}
|
||||||
|
if (m.audienceExpression != null && Object.hasOwnProperty.call(m, "audienceExpression"))
|
||||||
|
w.uint32(42).string(m.audienceExpression);
|
||||||
return w;
|
return w;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87647,6 +87647,10 @@ export const proto = $root.proto = (() => {
|
|||||||
m.labelIds.push(r.string());
|
m.labelIds.push(r.string());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 5: {
|
||||||
|
m.audienceExpression = r.string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
r.skipType(t & 7);
|
r.skipType(t & 7);
|
||||||
break;
|
break;
|
||||||
@@ -87683,6 +87687,9 @@ export const proto = $root.proto = (() => {
|
|||||||
m.labelIds[i] = String(d.labelIds[i]);
|
m.labelIds[i] = String(d.labelIds[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (d.audienceExpression != null) {
|
||||||
|
m.audienceExpression = String(d.audienceExpression);
|
||||||
|
}
|
||||||
return m;
|
return m;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87716,6 +87723,11 @@ export const proto = $root.proto = (() => {
|
|||||||
d.labelIds[j] = m.labelIds[j];
|
d.labelIds[j] = m.labelIds[j];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (m.audienceExpression != null && m.hasOwnProperty("audienceExpression")) {
|
||||||
|
d.audienceExpression = m.audienceExpression;
|
||||||
|
if (o.oneofs)
|
||||||
|
d._audienceExpression = "audienceExpression";
|
||||||
|
}
|
||||||
return d;
|
return d;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":[2,3000,1035504971]}
|
{"version":[2,3000,1035595667]}
|
||||||
|
|||||||
+41
-39
@@ -49,9 +49,12 @@ import {
|
|||||||
getStatusFromReceiptType,
|
getStatusFromReceiptType,
|
||||||
handleIdentityChange,
|
handleIdentityChange,
|
||||||
hkdf,
|
hkdf,
|
||||||
|
BAD_MAC_ERROR_TEXT,
|
||||||
|
DECRYPTION_RETRY_CONFIG,
|
||||||
MISSING_KEYS_ERROR_TEXT,
|
MISSING_KEYS_ERROR_TEXT,
|
||||||
NACK_REASONS,
|
NACK_REASONS,
|
||||||
NO_MESSAGE_FOUND_ERROR_TEXT,
|
NO_MESSAGE_FOUND_ERROR_TEXT,
|
||||||
|
RetryReason,
|
||||||
normalizeKeyLidToPn,
|
normalizeKeyLidToPn,
|
||||||
normalizeMessageJids,
|
normalizeMessageJids,
|
||||||
resolveLidToPn,
|
resolveLidToPn,
|
||||||
@@ -1213,7 +1216,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
return await query(stanza)
|
return await query(stanza)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendRetryRequest = async (node: BinaryNode, forceIncludeKeys = false) => {
|
const sendRetryRequest = async (node: BinaryNode, forceIncludeKeys = false, decryptionError?: string) => {
|
||||||
const { fullMessage } = decodeMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '')
|
const { fullMessage } = decodeMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '')
|
||||||
const { key: msgKey } = fullMessage
|
const { key: msgKey } = fullMessage
|
||||||
const msgId = msgKey.id!
|
const msgId = msgKey.id!
|
||||||
@@ -1311,39 +1314,39 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
|
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
|
||||||
const fromJid = node.attrs.from!
|
const fromJid = node.attrs.from!
|
||||||
|
|
||||||
// Check if we should recreate the session
|
// Derive the Signal error code from the actual decryption failure message.
|
||||||
let shouldRecreateSession = false
|
// Sent in the retry receipt so the peer (even another InfiniteAPI instance)
|
||||||
let recreateReason = ''
|
// knows the exact failure type and can recreate the session immediately
|
||||||
|
// instead of falling back to the 1-hour timeout.
|
||||||
if (enableAutoSessionRecreation && messageRetryManager && retryCount >= 1) {
|
//
|
||||||
try {
|
// Codes mirror RetryReason enum in message-retry-manager.ts:
|
||||||
// Check if we have a session with this JID
|
// 0 = UnknownError | 1 = NoSession | 2 = InvalidKey
|
||||||
const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
|
// 3 = InvalidKeyId | 4 = InvalidMessage | 7 = BadMac
|
||||||
const hasSession = await signalRepository.validateSession(fromJid)
|
//
|
||||||
|
// Uses DECRYPTION_RETRY_CONFIG error lists (single source of truth in
|
||||||
// Extract error code from retry node if present (for MAC error detection)
|
// decode-wa-message.ts) so additions to those lists are picked up here
|
||||||
const retryNode = getBinaryNodeChild(node, 'retry')
|
// automatically.
|
||||||
const errorAttr = retryNode?.attrs?.error
|
//
|
||||||
const errorCode = messageRetryManager.parseRetryErrorCode(errorAttr)
|
// NOTE: We do NOT delete the session here (receiver side). The Signal Protocol
|
||||||
|
// recovers automatically when the sender's pkmsg arrives — it overwrites the
|
||||||
const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists, errorCode)
|
// corrupted session. Deleting prematurely creates a race window where no session
|
||||||
shouldRecreateSession = result.recreate
|
// exists, which can cause "No Session" errors on concurrent messages.
|
||||||
recreateReason = result.reason
|
const retryErrorCode = (() => {
|
||||||
|
if (!decryptionError) return RetryReason.UnknownError
|
||||||
if (shouldRecreateSession) {
|
// Bad MAC must be checked first — it is also in corruptedSessionErrors
|
||||||
logger.debug({ fromJid, retryCount, reason: recreateReason, errorCode }, 'recreating session for retry')
|
// but warrants the more specific code 7 over the generic code 4.
|
||||||
// Delete existing session to force recreation
|
if (decryptionError.includes(BAD_MAC_ERROR_TEXT)) return RetryReason.SignalErrorBadMac
|
||||||
// CRITICAL: Use same transaction key as encrypt/decrypt operations to prevent race
|
// MessageCounterError and other corrupted-session variants
|
||||||
// Using meId ensures this delete serializes with sendMessage() and other session operations
|
if (DECRYPTION_RETRY_CONFIG.corruptedSessionErrors.some(e => decryptionError.includes(e))) return RetryReason.SignalErrorInvalidMessage
|
||||||
await authState.keys.transaction(async () => {
|
// Missing / invalid session record
|
||||||
await authState.keys.set({ session: { [sessionId]: null } })
|
if (DECRYPTION_RETRY_CONFIG.sessionRecordErrors.some(e => decryptionError.includes(e)) ||
|
||||||
}, authState.creds.me?.id || 'session-operation')
|
/no\s+(open\s+)?sessions?/i.test(decryptionError)) return RetryReason.SignalErrorNoSession
|
||||||
forceIncludeKeys = true
|
// PreKey / key-id errors
|
||||||
}
|
if (/pre\s*key/i.test(decryptionError)) return RetryReason.SignalErrorInvalidKeyId
|
||||||
} catch (error) {
|
// Identity / key errors
|
||||||
logger.warn({ error, fromJid }, 'failed to check session recreation')
|
if (/invalid\s*key|untrusted\s*identity/i.test(decryptionError)) return RetryReason.SignalErrorInvalidKey
|
||||||
}
|
return RetryReason.UnknownError
|
||||||
}
|
})()
|
||||||
|
|
||||||
if (retryCount <= 2) {
|
if (retryCount <= 2) {
|
||||||
// Use new retry manager for phone requests if available
|
// Use new retry manager for phone requests if available
|
||||||
@@ -1384,8 +1387,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
id: node.attrs.id!,
|
id: node.attrs.id!,
|
||||||
t: node.attrs.t!,
|
t: node.attrs.t!,
|
||||||
v: '1',
|
v: '1',
|
||||||
// ADD ERROR FIELD
|
error: retryErrorCode.toString()
|
||||||
error: '0'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1404,7 +1406,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
receipt.attrs.participant = node.attrs.participant
|
receipt.attrs.participant = node.attrs.participant
|
||||||
}
|
}
|
||||||
|
|
||||||
if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
|
if (retryCount > 1 || forceIncludeKeys) {
|
||||||
const { update, preKeys } = await getNextPreKeys(authState, 1)
|
const { update, preKeys } = await getNextPreKeys(authState, 1)
|
||||||
|
|
||||||
const [keyId] = Object.keys(preKeys)
|
const [keyId] = Object.keys(preKeys)
|
||||||
@@ -2507,7 +2509,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const encNode = getBinaryNodeChild(node, 'enc')
|
const encNode = getBinaryNodeChild(node, 'enc')
|
||||||
await sendRetryRequest(node, !encNode)
|
await sendRetryRequest(node, !encNode, errorMessage)
|
||||||
if (retryRequestDelayMs) {
|
if (retryRequestDelayMs) {
|
||||||
await delay(retryRequestDelayMs)
|
await delay(retryRequestDelayMs)
|
||||||
}
|
}
|
||||||
@@ -2516,7 +2518,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
// Still attempt retry even if pre-key upload failed
|
// Still attempt retry even if pre-key upload failed
|
||||||
try {
|
try {
|
||||||
const encNode = getBinaryNodeChild(node, 'enc')
|
const encNode = getBinaryNodeChild(node, 'enc')
|
||||||
await sendRetryRequest(node, !encNode)
|
await sendRetryRequest(node, !encNode, errorMessage)
|
||||||
} catch (retryErr) {
|
} catch (retryErr) {
|
||||||
logger.error({ retryErr }, 'Failed to send retry after error handling')
|
logger.error({ retryErr }, 'Failed to send retry after error handling')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1277,21 +1277,48 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
: otherRecipients
|
: otherRecipients
|
||||||
const effectiveAllRecipients = [...effectiveMeRecipients, ...effectiveOtherRecipients]
|
const effectiveAllRecipients = [...effectiveMeRecipients, ...effectiveOtherRecipients]
|
||||||
|
|
||||||
await assertSessions(effectiveAllRecipients)
|
// P2: detect actual view-once media by checking inner message's viewOnce flag.
|
||||||
|
// viewOnceMessage wrapper is also used for interactive messages (buttons, lists, etc)
|
||||||
|
// which do NOT carry viewOnce=true on the media — those must NOT be filtered.
|
||||||
|
const viewOnceInner =
|
||||||
|
message.viewOnceMessageV2?.message ||
|
||||||
|
message.viewOnceMessage?.message ||
|
||||||
|
message.viewOnceMessageV2Extension?.message
|
||||||
|
const isViewOnceMsg = !!(
|
||||||
|
viewOnceInner?.imageMessage?.viewOnce ||
|
||||||
|
viewOnceInner?.videoMessage?.viewOnce ||
|
||||||
|
viewOnceInner?.audioMessage?.viewOnce
|
||||||
|
)
|
||||||
|
|
||||||
|
// For view-once: only send DSM to primary phone (device=0).
|
||||||
|
// Companion devices (device>0) are omitted — WA server generates
|
||||||
|
// <unavailable type="view_once"/> for them automatically.
|
||||||
|
// Sending explicit <unavailable> from a companion is rejected by the server.
|
||||||
|
const viewOnceMeRecipients = isViewOnceMsg
|
||||||
|
? effectiveMeRecipients.filter(jid => !jidDecode(jid)?.device)
|
||||||
|
: effectiveMeRecipients
|
||||||
|
|
||||||
|
// P3: assert sessions only for recipients we actually encrypt for.
|
||||||
|
// For view-once, companions are omitted — asserting their sessions is wasteful
|
||||||
|
// and could block the send if a companion session is corrupted.
|
||||||
|
await assertSessions([...viewOnceMeRecipients, ...effectiveOtherRecipients])
|
||||||
|
|
||||||
const [
|
const [
|
||||||
{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 },
|
{ nodes: meNodes, shouldIncludeDeviceIdentity: s1 },
|
||||||
{ nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }
|
{ nodes: otherNodes, shouldIncludeDeviceIdentity: s2 }
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
// For own devices: use DSM (deviceSentMessage) wrapper
|
// For own devices: use DSM (deviceSentMessage) wrapper
|
||||||
createParticipantNodes(effectiveMeRecipients, meMsg || message, extraAttrs),
|
createParticipantNodes(viewOnceMeRecipients, meMsg || message, extraAttrs),
|
||||||
createParticipantNodes(effectiveOtherRecipients, message, extraAttrs)
|
createParticipantNodes(effectiveOtherRecipients, message, extraAttrs)
|
||||||
])
|
])
|
||||||
participants.push(...meNodes)
|
participants.push(...meNodes)
|
||||||
participants.push(...otherNodes)
|
participants.push(...otherNodes)
|
||||||
|
|
||||||
if (effectiveMeRecipients.length > 0 || effectiveOtherRecipients.length > 0) {
|
const phashRecipients = isViewOnceMsg
|
||||||
extraAttrs['phash'] = generateParticipantHashV2(effectiveAllRecipients)
|
? [...viewOnceMeRecipients, ...effectiveOtherRecipients]
|
||||||
|
: effectiveAllRecipients
|
||||||
|
if (phashRecipients.length > 0) {
|
||||||
|
extraAttrs['phash'] = generateParticipantHashV2(phashRecipients)
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
|
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
|
||||||
@@ -1897,6 +1924,15 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getMediaType = (message: proto.IMessage) => {
|
const getMediaType = (message: proto.IMessage) => {
|
||||||
|
// For view-once media, unwrap the viewOnceMessage wrapper before checking media type
|
||||||
|
const inner =
|
||||||
|
message.viewOnceMessage?.message ||
|
||||||
|
message.viewOnceMessageV2?.message ||
|
||||||
|
message.viewOnceMessageV2Extension?.message
|
||||||
|
if (inner) {
|
||||||
|
return getMediaType(inner)
|
||||||
|
}
|
||||||
|
|
||||||
if (message.imageMessage) {
|
if (message.imageMessage) {
|
||||||
return 'image'
|
return 'image'
|
||||||
} else if (message.videoMessage) {
|
} else if (message.videoMessage) {
|
||||||
|
|||||||
@@ -392,6 +392,23 @@ export const decryptMessageNode = (
|
|||||||
} else {
|
} else {
|
||||||
fullMessage.message = msg
|
fullMessage.message = msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect view-once media on stanza 1 received by linked device.
|
||||||
|
// viewOnceMessage wrapper is also used for interactive messages
|
||||||
|
// (interactiveMessage, listMessage, nativeFlowMessage) -- those do NOT have
|
||||||
|
// imageMessage.viewOnce / videoMessage.viewOnce / audioMessage.viewOnce = true.
|
||||||
|
// Only real view-once media carries viewOnce: true on the inner media message.
|
||||||
|
const viewOnceInner =
|
||||||
|
msg.viewOnceMessage?.message ||
|
||||||
|
msg.viewOnceMessageV2?.message ||
|
||||||
|
msg.viewOnceMessageV2Extension?.message
|
||||||
|
if (
|
||||||
|
viewOnceInner?.imageMessage?.viewOnce ||
|
||||||
|
viewOnceInner?.videoMessage?.viewOnce ||
|
||||||
|
viewOnceInner?.audioMessage?.viewOnce
|
||||||
|
) {
|
||||||
|
fullMessage.key.isViewOnce = true
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Check if this is a final failure after all retries exhausted
|
// Check if this is a final failure after all retries exhausted
|
||||||
const isRetryExhausted = err instanceof RetryExhaustedError
|
const isRetryExhausted = err instanceof RetryExhaustedError
|
||||||
|
|||||||
Reference in New Issue
Block a user