Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b4d0bf109 | |||
| 6a19615a16 | |||
| 9dfd31d109 | |||
| e686c5a2e1 | |||
| 466c14ff82 | |||
| 03bc03b318 | |||
| d83971d15e | |||
| 14f0321d7c | |||
| b65a8a334b | |||
| 28e9e5b912 | |||
| 962e7de0bf | |||
| c6cb29fd80 | |||
| a283d858c6 | |||
| 9c626801b0 | |||
| 4ca1878705 | |||
| 452df5d409 | |||
| e633d62a3b | |||
| 2a321b881e | |||
| 802d5f6efa | |||
| e0038bde36 | |||
| 1cbf1b18b7 | |||
| c3a021fcde | |||
| e6ea7e4563 | |||
| 3ab578aadf | |||
| 5926da9493 | |||
| 2b2c3f9155 | |||
| d077902695 | |||
| c4e5d1262a | |||
| 6afde71691 | |||
| 6060293793 | |||
| afce764ce9 | |||
| ad5ea817f7 | |||
| 3841abca35 |
+11
-1
@@ -1,7 +1,7 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
package proto;
|
package proto;
|
||||||
|
|
||||||
/// WhatsApp Version: 2.3000.1035577069
|
/// WhatsApp Version: 2.3000.1035715252
|
||||||
|
|
||||||
message ADVDeviceIdentity {
|
message ADVDeviceIdentity {
|
||||||
optional uint32 rawId = 1;
|
optional uint32 rawId = 1;
|
||||||
@@ -5304,11 +5304,21 @@ message SyncActionValue {
|
|||||||
repeated string userJid = 2;
|
repeated string userJid = 2;
|
||||||
optional bool shareToFB = 3;
|
optional bool shareToFB = 3;
|
||||||
optional bool shareToIG = 4;
|
optional bool shareToIG = 4;
|
||||||
|
repeated CustomList customLists = 5;
|
||||||
|
message CustomList {
|
||||||
|
optional int64 id = 1;
|
||||||
|
optional string name = 2;
|
||||||
|
optional string emoji = 3;
|
||||||
|
optional bool isSelected = 4;
|
||||||
|
repeated string userJid = 5;
|
||||||
|
}
|
||||||
|
|
||||||
enum StatusDistributionMode {
|
enum StatusDistributionMode {
|
||||||
ALLOW_LIST = 0;
|
ALLOW_LIST = 0;
|
||||||
DENY_LIST = 1;
|
DENY_LIST = 1;
|
||||||
CONTACTS = 2;
|
CONTACTS = 2;
|
||||||
CLOSE_FRIENDS = 3;
|
CLOSE_FRIENDS = 3;
|
||||||
|
CUSTOM_LIST = 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+28
-1
@@ -13702,6 +13702,7 @@ export namespace proto {
|
|||||||
userJid?: (string[]|null);
|
userJid?: (string[]|null);
|
||||||
shareToFB?: (boolean|null);
|
shareToFB?: (boolean|null);
|
||||||
shareToIG?: (boolean|null);
|
shareToIG?: (boolean|null);
|
||||||
|
customLists?: (proto.SyncActionValue.StatusPrivacyAction.ICustomList[]|null);
|
||||||
}
|
}
|
||||||
|
|
||||||
class StatusPrivacyAction implements IStatusPrivacyAction {
|
class StatusPrivacyAction implements IStatusPrivacyAction {
|
||||||
@@ -13710,6 +13711,7 @@ export namespace proto {
|
|||||||
public userJid: string[];
|
public userJid: string[];
|
||||||
public shareToFB?: (boolean|null);
|
public shareToFB?: (boolean|null);
|
||||||
public shareToIG?: (boolean|null);
|
public shareToIG?: (boolean|null);
|
||||||
|
public customLists: proto.SyncActionValue.StatusPrivacyAction.ICustomList[];
|
||||||
public static create(properties?: proto.SyncActionValue.IStatusPrivacyAction): proto.SyncActionValue.StatusPrivacyAction;
|
public static create(properties?: proto.SyncActionValue.IStatusPrivacyAction): proto.SyncActionValue.StatusPrivacyAction;
|
||||||
public static encode(m: proto.SyncActionValue.IStatusPrivacyAction, w?: $protobuf.Writer): $protobuf.Writer;
|
public static encode(m: proto.SyncActionValue.IStatusPrivacyAction, w?: $protobuf.Writer): $protobuf.Writer;
|
||||||
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.StatusPrivacyAction;
|
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.StatusPrivacyAction;
|
||||||
@@ -13721,11 +13723,36 @@ export namespace proto {
|
|||||||
|
|
||||||
namespace StatusPrivacyAction {
|
namespace StatusPrivacyAction {
|
||||||
|
|
||||||
|
interface ICustomList {
|
||||||
|
id?: (number|Long|null);
|
||||||
|
name?: (string|null);
|
||||||
|
emoji?: (string|null);
|
||||||
|
isSelected?: (boolean|null);
|
||||||
|
userJid?: (string[]|null);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomList implements ICustomList {
|
||||||
|
constructor(p?: proto.SyncActionValue.StatusPrivacyAction.ICustomList);
|
||||||
|
public id?: (number|Long|null);
|
||||||
|
public name?: (string|null);
|
||||||
|
public emoji?: (string|null);
|
||||||
|
public isSelected?: (boolean|null);
|
||||||
|
public userJid: string[];
|
||||||
|
public static create(properties?: proto.SyncActionValue.StatusPrivacyAction.ICustomList): proto.SyncActionValue.StatusPrivacyAction.CustomList;
|
||||||
|
public static encode(m: proto.SyncActionValue.StatusPrivacyAction.ICustomList, w?: $protobuf.Writer): $protobuf.Writer;
|
||||||
|
public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): proto.SyncActionValue.StatusPrivacyAction.CustomList;
|
||||||
|
public static fromObject(d: { [k: string]: any }): proto.SyncActionValue.StatusPrivacyAction.CustomList;
|
||||||
|
public static toObject(m: proto.SyncActionValue.StatusPrivacyAction.CustomList, o?: $protobuf.IConversionOptions): { [k: string]: any };
|
||||||
|
public toJSON(): { [k: string]: any };
|
||||||
|
public static getTypeUrl(typeUrlPrefix?: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
enum StatusDistributionMode {
|
enum StatusDistributionMode {
|
||||||
ALLOW_LIST = 0,
|
ALLOW_LIST = 0,
|
||||||
DENY_LIST = 1,
|
DENY_LIST = 1,
|
||||||
CONTACTS = 2,
|
CONTACTS = 2,
|
||||||
CLOSE_FRIENDS = 3
|
CLOSE_FRIENDS = 3,
|
||||||
|
CUSTOM_LIST = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94967,6 +94967,7 @@ export const proto = $root.proto = (() => {
|
|||||||
|
|
||||||
function StatusPrivacyAction(p) {
|
function StatusPrivacyAction(p) {
|
||||||
this.userJid = [];
|
this.userJid = [];
|
||||||
|
this.customLists = [];
|
||||||
if (p)
|
if (p)
|
||||||
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
|
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
|
||||||
if (p[ks[i]] != null)
|
if (p[ks[i]] != null)
|
||||||
@@ -94977,6 +94978,7 @@ export const proto = $root.proto = (() => {
|
|||||||
StatusPrivacyAction.prototype.userJid = $util.emptyArray;
|
StatusPrivacyAction.prototype.userJid = $util.emptyArray;
|
||||||
StatusPrivacyAction.prototype.shareToFB = null;
|
StatusPrivacyAction.prototype.shareToFB = null;
|
||||||
StatusPrivacyAction.prototype.shareToIG = null;
|
StatusPrivacyAction.prototype.shareToIG = null;
|
||||||
|
StatusPrivacyAction.prototype.customLists = $util.emptyArray;
|
||||||
|
|
||||||
let $oneOfFields;
|
let $oneOfFields;
|
||||||
|
|
||||||
@@ -95015,6 +95017,10 @@ export const proto = $root.proto = (() => {
|
|||||||
w.uint32(24).bool(m.shareToFB);
|
w.uint32(24).bool(m.shareToFB);
|
||||||
if (m.shareToIG != null && Object.hasOwnProperty.call(m, "shareToIG"))
|
if (m.shareToIG != null && Object.hasOwnProperty.call(m, "shareToIG"))
|
||||||
w.uint32(32).bool(m.shareToIG);
|
w.uint32(32).bool(m.shareToIG);
|
||||||
|
if (m.customLists != null && m.customLists.length) {
|
||||||
|
for (var i = 0; i < m.customLists.length; ++i)
|
||||||
|
$root.proto.SyncActionValue.StatusPrivacyAction.CustomList.encode(m.customLists[i], w.uint32(42).fork()).ldelim();
|
||||||
|
}
|
||||||
return w;
|
return w;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95045,6 +95051,12 @@ export const proto = $root.proto = (() => {
|
|||||||
m.shareToIG = r.bool();
|
m.shareToIG = r.bool();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 5: {
|
||||||
|
if (!(m.customLists && m.customLists.length))
|
||||||
|
m.customLists = [];
|
||||||
|
m.customLists.push($root.proto.SyncActionValue.StatusPrivacyAction.CustomList.decode(r, r.uint32()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
r.skipType(t & 7);
|
r.skipType(t & 7);
|
||||||
break;
|
break;
|
||||||
@@ -95080,6 +95092,10 @@ export const proto = $root.proto = (() => {
|
|||||||
case 3:
|
case 3:
|
||||||
m.mode = 3;
|
m.mode = 3;
|
||||||
break;
|
break;
|
||||||
|
case "CUSTOM_LIST":
|
||||||
|
case 4:
|
||||||
|
m.mode = 4;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (d.userJid) {
|
if (d.userJid) {
|
||||||
if (!Array.isArray(d.userJid))
|
if (!Array.isArray(d.userJid))
|
||||||
@@ -95095,6 +95111,16 @@ export const proto = $root.proto = (() => {
|
|||||||
if (d.shareToIG != null) {
|
if (d.shareToIG != null) {
|
||||||
m.shareToIG = Boolean(d.shareToIG);
|
m.shareToIG = Boolean(d.shareToIG);
|
||||||
}
|
}
|
||||||
|
if (d.customLists) {
|
||||||
|
if (!Array.isArray(d.customLists))
|
||||||
|
throw TypeError(".proto.SyncActionValue.StatusPrivacyAction.customLists: array expected");
|
||||||
|
m.customLists = [];
|
||||||
|
for (var i = 0; i < d.customLists.length; ++i) {
|
||||||
|
if (typeof d.customLists[i] !== "object")
|
||||||
|
throw TypeError(".proto.SyncActionValue.StatusPrivacyAction.customLists: object expected");
|
||||||
|
m.customLists[i] = $root.proto.SyncActionValue.StatusPrivacyAction.CustomList.fromObject(d.customLists[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
return m;
|
return m;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95104,6 +95130,7 @@ export const proto = $root.proto = (() => {
|
|||||||
var d = {};
|
var d = {};
|
||||||
if (o.arrays || o.defaults) {
|
if (o.arrays || o.defaults) {
|
||||||
d.userJid = [];
|
d.userJid = [];
|
||||||
|
d.customLists = [];
|
||||||
}
|
}
|
||||||
if (m.mode != null && m.hasOwnProperty("mode")) {
|
if (m.mode != null && m.hasOwnProperty("mode")) {
|
||||||
d.mode = o.enums === String ? $root.proto.SyncActionValue.StatusPrivacyAction.StatusDistributionMode[m.mode] === undefined ? m.mode : $root.proto.SyncActionValue.StatusPrivacyAction.StatusDistributionMode[m.mode] : m.mode;
|
d.mode = o.enums === String ? $root.proto.SyncActionValue.StatusPrivacyAction.StatusDistributionMode[m.mode] === undefined ? m.mode : $root.proto.SyncActionValue.StatusPrivacyAction.StatusDistributionMode[m.mode] : m.mode;
|
||||||
@@ -95126,6 +95153,12 @@ export const proto = $root.proto = (() => {
|
|||||||
if (o.oneofs)
|
if (o.oneofs)
|
||||||
d._shareToIG = "shareToIG";
|
d._shareToIG = "shareToIG";
|
||||||
}
|
}
|
||||||
|
if (m.customLists && m.customLists.length) {
|
||||||
|
d.customLists = [];
|
||||||
|
for (var j = 0; j < m.customLists.length; ++j) {
|
||||||
|
d.customLists[j] = $root.proto.SyncActionValue.StatusPrivacyAction.CustomList.toObject(m.customLists[j], o);
|
||||||
|
}
|
||||||
|
}
|
||||||
return d;
|
return d;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95140,12 +95173,203 @@ export const proto = $root.proto = (() => {
|
|||||||
return typeUrlPrefix + "/proto.SyncActionValue.StatusPrivacyAction";
|
return typeUrlPrefix + "/proto.SyncActionValue.StatusPrivacyAction";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
StatusPrivacyAction.CustomList = (function() {
|
||||||
|
|
||||||
|
function CustomList(p) {
|
||||||
|
this.userJid = [];
|
||||||
|
if (p)
|
||||||
|
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
|
||||||
|
if (p[ks[i]] != null)
|
||||||
|
this[ks[i]] = p[ks[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomList.prototype.id = null;
|
||||||
|
CustomList.prototype.name = null;
|
||||||
|
CustomList.prototype.emoji = null;
|
||||||
|
CustomList.prototype.isSelected = null;
|
||||||
|
CustomList.prototype.userJid = $util.emptyArray;
|
||||||
|
|
||||||
|
let $oneOfFields;
|
||||||
|
|
||||||
|
// Virtual OneOf for proto3 optional field
|
||||||
|
Object.defineProperty(CustomList.prototype, "_id", {
|
||||||
|
get: $util.oneOfGetter($oneOfFields = ["id"]),
|
||||||
|
set: $util.oneOfSetter($oneOfFields)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Virtual OneOf for proto3 optional field
|
||||||
|
Object.defineProperty(CustomList.prototype, "_name", {
|
||||||
|
get: $util.oneOfGetter($oneOfFields = ["name"]),
|
||||||
|
set: $util.oneOfSetter($oneOfFields)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Virtual OneOf for proto3 optional field
|
||||||
|
Object.defineProperty(CustomList.prototype, "_emoji", {
|
||||||
|
get: $util.oneOfGetter($oneOfFields = ["emoji"]),
|
||||||
|
set: $util.oneOfSetter($oneOfFields)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Virtual OneOf for proto3 optional field
|
||||||
|
Object.defineProperty(CustomList.prototype, "_isSelected", {
|
||||||
|
get: $util.oneOfGetter($oneOfFields = ["isSelected"]),
|
||||||
|
set: $util.oneOfSetter($oneOfFields)
|
||||||
|
});
|
||||||
|
|
||||||
|
CustomList.create = function create(properties) {
|
||||||
|
return new CustomList(properties);
|
||||||
|
};
|
||||||
|
|
||||||
|
CustomList.encode = function encode(m, w) {
|
||||||
|
if (!w)
|
||||||
|
w = $Writer.create();
|
||||||
|
if (m.id != null && Object.hasOwnProperty.call(m, "id"))
|
||||||
|
w.uint32(8).int64(m.id);
|
||||||
|
if (m.name != null && Object.hasOwnProperty.call(m, "name"))
|
||||||
|
w.uint32(18).string(m.name);
|
||||||
|
if (m.emoji != null && Object.hasOwnProperty.call(m, "emoji"))
|
||||||
|
w.uint32(26).string(m.emoji);
|
||||||
|
if (m.isSelected != null && Object.hasOwnProperty.call(m, "isSelected"))
|
||||||
|
w.uint32(32).bool(m.isSelected);
|
||||||
|
if (m.userJid != null && m.userJid.length) {
|
||||||
|
for (var i = 0; i < m.userJid.length; ++i)
|
||||||
|
w.uint32(42).string(m.userJid[i]);
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
};
|
||||||
|
|
||||||
|
CustomList.decode = function decode(r, l, e) {
|
||||||
|
if (!(r instanceof $Reader))
|
||||||
|
r = $Reader.create(r);
|
||||||
|
var c = l === undefined ? r.len : r.pos + l, m = new $root.proto.SyncActionValue.StatusPrivacyAction.CustomList();
|
||||||
|
while (r.pos < c) {
|
||||||
|
var t = r.uint32();
|
||||||
|
if (t === e)
|
||||||
|
break;
|
||||||
|
switch (t >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
m.id = r.int64();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
m.name = r.string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
m.emoji = r.string();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
m.isSelected = r.bool();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 5: {
|
||||||
|
if (!(m.userJid && m.userJid.length))
|
||||||
|
m.userJid = [];
|
||||||
|
m.userJid.push(r.string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
r.skipType(t & 7);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
|
||||||
|
CustomList.fromObject = function fromObject(d) {
|
||||||
|
if (d instanceof $root.proto.SyncActionValue.StatusPrivacyAction.CustomList)
|
||||||
|
return d;
|
||||||
|
var m = new $root.proto.SyncActionValue.StatusPrivacyAction.CustomList();
|
||||||
|
if (d.id != null) {
|
||||||
|
if ($util.Long)
|
||||||
|
(m.id = $util.Long.fromValue(d.id)).unsigned = false;
|
||||||
|
else if (typeof d.id === "string")
|
||||||
|
m.id = parseInt(d.id, 10);
|
||||||
|
else if (typeof d.id === "number")
|
||||||
|
m.id = d.id;
|
||||||
|
else if (typeof d.id === "object")
|
||||||
|
m.id = new $util.LongBits(d.id.low >>> 0, d.id.high >>> 0).toNumber();
|
||||||
|
}
|
||||||
|
if (d.name != null) {
|
||||||
|
m.name = String(d.name);
|
||||||
|
}
|
||||||
|
if (d.emoji != null) {
|
||||||
|
m.emoji = String(d.emoji);
|
||||||
|
}
|
||||||
|
if (d.isSelected != null) {
|
||||||
|
m.isSelected = Boolean(d.isSelected);
|
||||||
|
}
|
||||||
|
if (d.userJid) {
|
||||||
|
if (!Array.isArray(d.userJid))
|
||||||
|
throw TypeError(".proto.SyncActionValue.StatusPrivacyAction.CustomList.userJid: array expected");
|
||||||
|
m.userJid = [];
|
||||||
|
for (var i = 0; i < d.userJid.length; ++i) {
|
||||||
|
m.userJid[i] = String(d.userJid[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
|
||||||
|
CustomList.toObject = function toObject(m, o) {
|
||||||
|
if (!o)
|
||||||
|
o = {};
|
||||||
|
var d = {};
|
||||||
|
if (o.arrays || o.defaults) {
|
||||||
|
d.userJid = [];
|
||||||
|
}
|
||||||
|
if (m.id != null && m.hasOwnProperty("id")) {
|
||||||
|
if (typeof m.id === "number")
|
||||||
|
d.id = o.longs === String ? String(m.id) : m.id;
|
||||||
|
else
|
||||||
|
d.id = o.longs === String ? longToString(m.id) : o.longs === Number ? longToNumber(m.id) : m.id;
|
||||||
|
if (o.oneofs)
|
||||||
|
d._id = "id";
|
||||||
|
}
|
||||||
|
if (m.name != null && m.hasOwnProperty("name")) {
|
||||||
|
d.name = m.name;
|
||||||
|
if (o.oneofs)
|
||||||
|
d._name = "name";
|
||||||
|
}
|
||||||
|
if (m.emoji != null && m.hasOwnProperty("emoji")) {
|
||||||
|
d.emoji = m.emoji;
|
||||||
|
if (o.oneofs)
|
||||||
|
d._emoji = "emoji";
|
||||||
|
}
|
||||||
|
if (m.isSelected != null && m.hasOwnProperty("isSelected")) {
|
||||||
|
d.isSelected = m.isSelected;
|
||||||
|
if (o.oneofs)
|
||||||
|
d._isSelected = "isSelected";
|
||||||
|
}
|
||||||
|
if (m.userJid && m.userJid.length) {
|
||||||
|
d.userJid = [];
|
||||||
|
for (var j = 0; j < m.userJid.length; ++j) {
|
||||||
|
d.userJid[j] = m.userJid[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
|
||||||
|
CustomList.prototype.toJSON = function toJSON() {
|
||||||
|
return this.constructor.toObject(this, $protobuf.util.toJSONOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
CustomList.getTypeUrl = function getTypeUrl(typeUrlPrefix) {
|
||||||
|
if (typeUrlPrefix === undefined) {
|
||||||
|
typeUrlPrefix = "type.googleapis.com";
|
||||||
|
}
|
||||||
|
return typeUrlPrefix + "/proto.SyncActionValue.StatusPrivacyAction.CustomList";
|
||||||
|
};
|
||||||
|
|
||||||
|
return CustomList;
|
||||||
|
})();
|
||||||
|
|
||||||
StatusPrivacyAction.StatusDistributionMode = (function() {
|
StatusPrivacyAction.StatusDistributionMode = (function() {
|
||||||
const valuesById = {}, values = Object.create(valuesById);
|
const valuesById = {}, values = Object.create(valuesById);
|
||||||
values[valuesById[0] = "ALLOW_LIST"] = 0;
|
values[valuesById[0] = "ALLOW_LIST"] = 0;
|
||||||
values[valuesById[1] = "DENY_LIST"] = 1;
|
values[valuesById[1] = "DENY_LIST"] = 1;
|
||||||
values[valuesById[2] = "CONTACTS"] = 2;
|
values[valuesById[2] = "CONTACTS"] = 2;
|
||||||
values[valuesById[3] = "CLOSE_FRIENDS"] = 3;
|
values[valuesById[3] = "CLOSE_FRIENDS"] = 3;
|
||||||
|
values[valuesById[4] = "CUSTOM_LIST"] = 4;
|
||||||
return values;
|
return values;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
+753
-437
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,531 @@
|
|||||||
|
# Mensagens View-Once (Visualização Única) — Guia de Implementação
|
||||||
|
|
||||||
|
**Data:** 2026-03-23
|
||||||
|
**Versão:** 1.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. O que é View-Once
|
||||||
|
|
||||||
|
Mensagem de visualização única é um tipo especial de mídia (imagem, vídeo ou áudio) que o destinatário pode abrir apenas uma vez. Após abrir, o conteúdo some permanentemente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> ## ⚠️ RESTRIÇÃO CRÍTICA — Contas Hosted (Meta Business Cloud API)
|
||||||
|
>
|
||||||
|
> **Contas registradas como "hosted" na Meta Business Cloud API têm view-once bloqueado pelo servidor do WhatsApp.**
|
||||||
|
>
|
||||||
|
> Essa restrição se aplica a **todos os clientes** (Android, iOS, WA Web) e **não há como contornar por código** — é uma política imposta diretamente pelo servidor WA para contas hosted.
|
||||||
|
>
|
||||||
|
> **Como identificar se sua conta é hosted:**
|
||||||
|
> - A conta foi criada via Meta Business Cloud API (WABA)
|
||||||
|
> - Aparece como "Meta" ou "Business Cloud" nas configurações do WhatsApp Business Manager
|
||||||
|
>
|
||||||
|
> **Contas não-hosted** (registradas diretamente via QR code ou pair code) **funcionam normalmente** com view-once.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Comportamento em cada dispositivo após envio via API (contas não-hosted):**
|
||||||
|
|
||||||
|
| Dispositivo | O que aparece |
|
||||||
|
|---|---|
|
||||||
|
| Destinatário (Android/iOS) | Recebe a mídia com ícone de olhinho — pode abrir uma vez |
|
||||||
|
| Android principal do remetente | Mostra na conversa que a mensagem foi enviada (ícone de olhinho) |
|
||||||
|
| WA Web do remetente | "Aguardando mensagem. Abra o WhatsApp no seu celular." |
|
||||||
|
|
||||||
|
> **Nota:** A mensagem "Aguardando mensagem..." no WA Web é o comportamento correto e esperado quando o envio vem de um companion (API). É uma limitação do protocolo WhatsApp — o servidor só permite que o celular principal notifique companions com o conteúdo da view-once.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Como Enviar via API
|
||||||
|
|
||||||
|
### 2.1 Imagem
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://sua-api.com/v1/messages/send_image \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: SEU_API_KEY" \
|
||||||
|
-d '{
|
||||||
|
"instance": "nome-da-instancia",
|
||||||
|
"to": "5511999999999",
|
||||||
|
"caption": "Olha só isso!",
|
||||||
|
"imageUrl": "https://exemplo.com/foto.jpg",
|
||||||
|
"viewOnce": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resposta de sucesso:**
|
||||||
|
```json
|
||||||
|
{ "ok": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Vídeo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://sua-api.com/v1/messages/send_video \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: SEU_API_KEY" \
|
||||||
|
-d '{
|
||||||
|
"instance": "nome-da-instancia",
|
||||||
|
"to": "5511999999999",
|
||||||
|
"caption": "Assista uma vez",
|
||||||
|
"videoUrl": "https://exemplo.com/video.mp4",
|
||||||
|
"viewOnce": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Áudio
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://sua-api.com/v1/messages/send_audio \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: SEU_API_KEY" \
|
||||||
|
-d '{
|
||||||
|
"instance": "nome-da-instancia",
|
||||||
|
"to": "5511999999999",
|
||||||
|
"audioUrl": "https://exemplo.com/audio.ogg",
|
||||||
|
"ptt": false,
|
||||||
|
"viewOnce": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Usando Base64 (sem URL pública)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://sua-api.com/v1/messages/send_image \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: SEU_API_KEY" \
|
||||||
|
-d '{
|
||||||
|
"instance": "nome-da-instancia",
|
||||||
|
"to": "5511999999999",
|
||||||
|
"imageBase64": "data:image/jpeg;base64,/9j/4AAQSkZJRgAB...",
|
||||||
|
"viewOnce": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Como Receber via Webhook
|
||||||
|
|
||||||
|
Quando um contato envia uma mensagem de visualização única para o número conectado na API, o webhook recebe o evento com `key.isViewOnce = true`.
|
||||||
|
|
||||||
|
### 3.1 Estrutura do evento recebido
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "messages.upsert",
|
||||||
|
"instance": "nome-da-instancia",
|
||||||
|
"data": {
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"remoteJid": "5511999999999@s.whatsapp.net",
|
||||||
|
"fromMe": false,
|
||||||
|
"id": "3EB0ABCDEF123456",
|
||||||
|
"isViewOnce": true
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"viewOnceMessageV2": {
|
||||||
|
"message": {
|
||||||
|
"imageMessage": {
|
||||||
|
"url": "https://mmg.whatsapp.net/...",
|
||||||
|
"mimetype": "image/jpeg",
|
||||||
|
"mediaKey": "base64...",
|
||||||
|
"fileEncSha256": "base64...",
|
||||||
|
"directPath": "/v/...",
|
||||||
|
"viewOnce": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messageTimestamp": 1742700000,
|
||||||
|
"pushName": "João Silva"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "notify"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Como identificar que é view-once
|
||||||
|
|
||||||
|
O campo principal para identificar é `key.isViewOnce = true`. Existem dois caminhos para confirmar:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function isViewOnce(msg) {
|
||||||
|
// Caminho 1: flag direta (mais simples)
|
||||||
|
if (msg.key?.isViewOnce) return true
|
||||||
|
|
||||||
|
// Caminho 2: verificar o wrapper da mensagem
|
||||||
|
const inner =
|
||||||
|
msg.message?.viewOnceMessageV2?.message ||
|
||||||
|
msg.message?.viewOnceMessage?.message ||
|
||||||
|
msg.message?.viewOnceMessageV2Extension?.message
|
||||||
|
|
||||||
|
return !!(
|
||||||
|
inner?.imageMessage?.viewOnce ||
|
||||||
|
inner?.videoMessage?.viewOnce ||
|
||||||
|
inner?.audioMessage?.viewOnce
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Como acessar a mídia
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function getViewOnceMedia(msg) {
|
||||||
|
const inner =
|
||||||
|
msg.message?.viewOnceMessageV2?.message ||
|
||||||
|
msg.message?.viewOnceMessage?.message ||
|
||||||
|
msg.message?.viewOnceMessageV2Extension?.message
|
||||||
|
|
||||||
|
if (!inner) return null
|
||||||
|
|
||||||
|
if (inner.imageMessage) return { type: 'image', media: inner.imageMessage }
|
||||||
|
if (inner.videoMessage) return { type: 'video', media: inner.videoMessage }
|
||||||
|
if (inner.audioMessage) return { type: 'audio', media: inner.audioMessage }
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uso:
|
||||||
|
const media = getViewOnceMedia(msg)
|
||||||
|
if (media) {
|
||||||
|
console.log(`View-once ${media.type}`)
|
||||||
|
console.log('URL:', media.media.url)
|
||||||
|
console.log('DirectPath:', media.media.directPath)
|
||||||
|
console.log('MediaKey:', media.media.mediaKey) // necessário para download
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Como Exibir numa Ferramenta (Frontend/CRM)
|
||||||
|
|
||||||
|
### 4.1 Lógica de detecção e exibição
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function renderMessage(msg) {
|
||||||
|
if (!isViewOnce(msg)) {
|
||||||
|
// Renderizar mensagem normal
|
||||||
|
return renderNormalMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
const media = getViewOnceMedia(msg)
|
||||||
|
if (!media) return
|
||||||
|
|
||||||
|
// Verificar se já foi aberta (controle local da ferramenta)
|
||||||
|
const alreadyOpened = localStorage.getItem(`vo_${msg.key.id}`)
|
||||||
|
|
||||||
|
if (alreadyOpened) {
|
||||||
|
// Mostrar placeholder de "já visualizado"
|
||||||
|
return renderViewOncePlaceholder(media.type, msg.key.fromMe)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.key.fromMe) {
|
||||||
|
// Mensagem enviada por mim — nunca mostrar conteúdo
|
||||||
|
return renderSentViewOnce(media.type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mensagem recebida — mostrar botão para "abrir uma vez"
|
||||||
|
return renderViewOnceButton(media.type, msg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Componente de exibição — mensagem recebida
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function renderViewOnceButton(type, msg) {
|
||||||
|
const icons = { image: '📷', video: '🎥', audio: '🎵' }
|
||||||
|
const labels = { image: 'Foto', video: 'Vídeo', áudio: 'Áudio' }
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="view-once-bubble received">
|
||||||
|
<span class="view-once-icon">${icons[type]}</span>
|
||||||
|
<span class="view-once-label">${labels[type]} de visualização única</span>
|
||||||
|
<button onclick="openViewOnce('${msg.key.id}', '${type}')">
|
||||||
|
Abrir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openViewOnce(msgId, type) {
|
||||||
|
// 1. Marcar como aberta ANTES de mostrar (só uma chance)
|
||||||
|
localStorage.setItem(`vo_${msgId}`, '1')
|
||||||
|
|
||||||
|
// 2. Baixar e mostrar a mídia
|
||||||
|
const mediaUrl = await downloadViewOnceMedia(msgId)
|
||||||
|
showMedia(type, mediaUrl)
|
||||||
|
|
||||||
|
// 3. Após fechar/visualizar, substituir pelo placeholder
|
||||||
|
onMediaClosed(() => {
|
||||||
|
replaceWithPlaceholder(msgId, type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Componente de exibição — mensagem enviada
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function renderSentViewOnce(type) {
|
||||||
|
const labels = { image: 'Foto', video: 'Vídeo', audio: 'Áudio' }
|
||||||
|
return `
|
||||||
|
<div class="view-once-bubble sent">
|
||||||
|
<span class="view-once-icon">👁️</span>
|
||||||
|
<span>${labels[type]} de visualização única</span>
|
||||||
|
<span class="view-once-status">Enviada</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Placeholder após visualização
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function renderViewOncePlaceholder(type, fromMe) {
|
||||||
|
const labels = { image: 'foto', video: 'vídeo', audio: 'áudio' }
|
||||||
|
const text = fromMe
|
||||||
|
? `Você enviou uma ${labels[type]} de visualização única`
|
||||||
|
: `${labels[type]} de visualização única aberta`
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="view-once-bubble placeholder">
|
||||||
|
<span class="view-once-icon">🚫</span>
|
||||||
|
<span>${text}</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 CSS sugerido
|
||||||
|
|
||||||
|
```css
|
||||||
|
.view-once-bubble {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-once-bubble.received {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-once-bubble.sent {
|
||||||
|
background: #dcf8c6;
|
||||||
|
color: #333;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-once-bubble.placeholder {
|
||||||
|
background: #e8e8e8;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-once-bubble button {
|
||||||
|
background: #25d366;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Código Implementado na API
|
||||||
|
|
||||||
|
### 5.1 Geração da mensagem — `src/Utils/messages.ts`
|
||||||
|
|
||||||
|
Quando `viewOnce: true` é passado, a mídia é encapsulada no wrapper moderno `viewOnceMessageV2` (campo 55 do proto) e o flag `viewOnce=true` é setado na mídia interna:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
|
||||||
|
// Seta viewOnce=true na mensagem de mídia interna
|
||||||
|
if (m.imageMessage) m.imageMessage.viewOnce = true
|
||||||
|
else if (m.videoMessage) m.videoMessage.viewOnce = true
|
||||||
|
else if (m.audioMessage) m.audioMessage.viewOnce = true
|
||||||
|
// Usa viewOnceMessageV2 (campo 55) — formato atual do WA
|
||||||
|
m = { viewOnceMessageV2: { message: m } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Envio — `src/Socket/messages-send.ts`
|
||||||
|
|
||||||
|
**Detecção de view-once real** (não confunde com botões/listas que também usam o wrapper `viewOnceMessage`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Detecta view-once real pela flag viewOnce na mídia interna.
|
||||||
|
// viewOnceMessage* wrappers são também usados por botões/listas/carrosséis
|
||||||
|
// (que carregam interactiveMessage dentro) — esses NÃO têm viewOnce=true na mídia.
|
||||||
|
const viewOnceInner =
|
||||||
|
message.viewOnceMessageV2?.message ||
|
||||||
|
message.viewOnceMessage?.message ||
|
||||||
|
message.viewOnceMessageV2Extension?.message
|
||||||
|
const isViewOnceMsg = !!(
|
||||||
|
viewOnceInner?.imageMessage?.viewOnce ||
|
||||||
|
viewOnceInner?.videoMessage?.viewOnce ||
|
||||||
|
viewOnceInner?.audioMessage?.viewOnce
|
||||||
|
)
|
||||||
|
|
||||||
|
// Para view-once: DSM enviado apenas para o celular principal (device=0).
|
||||||
|
// Companions (device>0) são omitidos — o servidor WA gera
|
||||||
|
// <unavailable type="view_once"/> automaticamente para eles.
|
||||||
|
const viewOnceMeRecipients = isViewOnceMsg
|
||||||
|
? meRecipients.filter(jid => !jidDecode(jid)?.device)
|
||||||
|
: meRecipients
|
||||||
|
|
||||||
|
// assertSessions apenas para quem vai realmente receber
|
||||||
|
await assertSessions([...viewOnceMeRecipients, ...otherRecipients])
|
||||||
|
|
||||||
|
const [meNodes, otherNodes] = await Promise.all([
|
||||||
|
createParticipantNodes(viewOnceMeRecipients, meMsg || message, extraAttrs),
|
||||||
|
createParticipantNodes(otherRecipients, message, extraAttrs)
|
||||||
|
])
|
||||||
|
participants.push(...meNodes)
|
||||||
|
participants.push(...otherNodes)
|
||||||
|
|
||||||
|
// phash recalculado com os participantes reais
|
||||||
|
const phashRecipients = isViewOnceMsg
|
||||||
|
? [...viewOnceMeRecipients, ...otherRecipients]
|
||||||
|
: [...meRecipients, ...otherRecipients]
|
||||||
|
if (phashRecipients.length > 0) {
|
||||||
|
extraAttrs['phash'] = generateParticipantHashV2(phashRecipients)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`getMediaType()` — garante que vídeo e áudio view-once sejam entregues:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const getMediaType = (message: proto.IMessage) => {
|
||||||
|
// Desencapsula wrapper view-once antes de verificar o tipo de mídia.
|
||||||
|
// Sem isso, message.videoMessage e message.audioMessage são undefined
|
||||||
|
// (estão dentro do wrapper) e o atributo mediatype ficaria ausente no
|
||||||
|
// enc node — o servidor WA descarta silenciosamente vídeo/áudio view-once.
|
||||||
|
const inner =
|
||||||
|
message.viewOnceMessage?.message ||
|
||||||
|
message.viewOnceMessageV2?.message ||
|
||||||
|
message.viewOnceMessageV2Extension?.message
|
||||||
|
if (inner) {
|
||||||
|
return getMediaType(inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.imageMessage) return 'image'
|
||||||
|
else if (message.videoMessage) return 'video'
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Recepção — `src/Utils/decode-wa-message.ts`
|
||||||
|
|
||||||
|
Quando a API recebe uma view-once como companion (linked device), o servidor envia dois stanzas:
|
||||||
|
|
||||||
|
- **Stanza 1** (`enc`): conteúdo completo com metadados da mídia
|
||||||
|
- **Stanza 2** (`unavailable type="view_once"`): sinal de sync
|
||||||
|
|
||||||
|
O stanza 1 é detectado e marcado corretamente:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Detecta view-once na stanza 1 recebida por linked device.
|
||||||
|
// O wrapper viewOnceMessage também é usado por mensagens interativas
|
||||||
|
// (interactiveMessage, listMessage) — esses NÃO têm viewOnce=true na mídia.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Fluxo Completo
|
||||||
|
|
||||||
|
```
|
||||||
|
API envia view-once para destinatário
|
||||||
|
│
|
||||||
|
├── Protobuf gerado:
|
||||||
|
│ viewOnceMessageV2 {
|
||||||
|
│ message {
|
||||||
|
│ imageMessage { viewOnce: true, url, mediaKey, ... }
|
||||||
|
│ }
|
||||||
|
│ }
|
||||||
|
│
|
||||||
|
├── Stanza enviado ao servidor WA:
|
||||||
|
│ <message to="destinatario@s.whatsapp.net" type="media">
|
||||||
|
│ <enc type="pkmsg">...viewOnceMessageV2 cifrado...</enc> ← para o destinatário
|
||||||
|
│ <participants>
|
||||||
|
│ <to jid="meu-numero:0@lid"> ← celular principal
|
||||||
|
│ <enc type="msg">...DSM{viewOnceMessageV2}...</enc>
|
||||||
|
│ </to>
|
||||||
|
│ <!-- device>0 omitidos — servidor gera unavailable -->
|
||||||
|
│ </participants>
|
||||||
|
│ </message>
|
||||||
|
│
|
||||||
|
└── Distribuição pelo servidor:
|
||||||
|
├── Destinatário → recebe enc → abre uma vez ✅
|
||||||
|
├── Celular principal → recebe DSM → mostra "você enviou" ✅
|
||||||
|
└── WA Web → recebe <unavailable> → "Aguardando..." ✅
|
||||||
|
|
||||||
|
|
||||||
|
Destinatário envia view-once de volta para a API
|
||||||
|
│
|
||||||
|
├── Stanza recebido pela API (stanza 1 — enc com conteúdo):
|
||||||
|
│ <message from="destinatario@s.whatsapp.net" type="media">
|
||||||
|
│ <enc type="msg">...viewOnceMessageV2 cifrado...</enc>
|
||||||
|
│ </message>
|
||||||
|
│
|
||||||
|
├── decode-wa-message.ts:
|
||||||
|
│ └── Descriptografa → detecta viewOnce=true → seta key.isViewOnce=true
|
||||||
|
│
|
||||||
|
├── Stanza recebido (stanza 2 — unavailable, ignorado):
|
||||||
|
│ <message from="...">
|
||||||
|
│ <unavailable type="view_once"/>
|
||||||
|
│ </message>
|
||||||
|
│
|
||||||
|
└── Webhook emitido:
|
||||||
|
messages.upsert → msg.key.isViewOnce = true ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Limitações
|
||||||
|
|
||||||
|
| Situação | Comportamento | Motivo |
|
||||||
|
|---|---|---|
|
||||||
|
| **Conta hosted (Meta Business Cloud API)** | **View-once bloqueado para todos os clientes** | **Política do servidor WA para contas hosted — sem workaround** |
|
||||||
|
| WA Web do remetente | "Aguardando mensagem. Abra no celular." | Servidor WA só aceita `<unavailable>` vindo do celular principal, não de companions |
|
||||||
|
| View-once de documento/sticker | Não suportado | Limitação do protocolo WA — só imagem, vídeo e áudio |
|
||||||
|
| `error_479` nos logs | Normal, não é erro | TcToken é atualizado após o envio de view-once |
|
||||||
|
| Companion não consegue abrir | Por design | O conteúdo não é entregue para linked devices |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Checklist de Validação
|
||||||
|
|
||||||
|
**Envio:**
|
||||||
|
- [ ] Destinatário recebe com ícone de olhinho
|
||||||
|
- [ ] Destinatário consegue abrir a mídia
|
||||||
|
- [ ] Após abrir, a mídia some
|
||||||
|
- [ ] Celular principal do remetente mostra "você enviou" com ícone de olhinho
|
||||||
|
- [ ] WA Web do remetente mostra "Aguardando mensagem..."
|
||||||
|
- [ ] Funciona para imagem, vídeo e áudio
|
||||||
|
- [ ] Logs mostram `📤 Message sent` (error_479 é normal)
|
||||||
|
|
||||||
|
**Recepção:**
|
||||||
|
- [ ] Webhook recebe `key.isViewOnce = true`
|
||||||
|
- [ ] `viewOnceMessageV2.message.imageMessage` (ou video/audio) está presente
|
||||||
|
- [ ] API não crasha ao receber view-once de outro dispositivo
|
||||||
|
- [ ] Mensagem interativa (botão/lista) enviada após view-once não é afetada
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":[2,3000,1035595667]}
|
{"version":[2,3000,1035913242]}
|
||||||
|
|||||||
+19
-2
@@ -154,7 +154,20 @@ export const MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
|
|||||||
'md-msg-hist': '/mms/md-app-state',
|
'md-msg-hist': '/mms/md-app-state',
|
||||||
'biz-cover-photo': '/pps/biz-cover-photo',
|
'biz-cover-photo': '/pps/biz-cover-photo',
|
||||||
'sticker-pack': '/mms/sticker-pack',
|
'sticker-pack': '/mms/sticker-pack',
|
||||||
'thumbnail-sticker-pack': '/mms/thumbnail-sticker-pack'
|
'thumbnail-sticker-pack': '/mms/thumbnail-sticker-pack',
|
||||||
|
// View-once types — same upload path as regular media (server CDN routing TBD)
|
||||||
|
'viewonce-image': '/mms/image',
|
||||||
|
'viewonce-video': '/mms/video',
|
||||||
|
'viewonce-audio': '/mms/audio'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NEWSLETTER_MEDIA_PATH_MAP: { [T in MediaType]?: string } = {
|
||||||
|
image: '/newsletter/newsletter-image',
|
||||||
|
video: '/newsletter/newsletter-video',
|
||||||
|
document: '/newsletter/newsletter-document',
|
||||||
|
audio: '/newsletter/newsletter-audio',
|
||||||
|
sticker: '/newsletter/newsletter-image',
|
||||||
|
'thumbnail-link': '/newsletter/newsletter-image'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MEDIA_HKDF_KEY_MAPPING = {
|
export const MEDIA_HKDF_KEY_MAPPING = {
|
||||||
@@ -178,7 +191,11 @@ export const MEDIA_HKDF_KEY_MAPPING = {
|
|||||||
ptv: 'Video',
|
ptv: 'Video',
|
||||||
'biz-cover-photo': 'Image',
|
'biz-cover-photo': 'Image',
|
||||||
'sticker-pack': 'Sticker Pack',
|
'sticker-pack': 'Sticker Pack',
|
||||||
'thumbnail-sticker-pack': 'Sticker Pack Thumbnail'
|
'thumbnail-sticker-pack': 'Sticker Pack Thumbnail',
|
||||||
|
// View-once types — same HKDF keys as regular types (recipients decrypt with normal keys)
|
||||||
|
'viewonce-image': 'Image',
|
||||||
|
'viewonce-video': 'Video',
|
||||||
|
'viewonce-audio': 'Audio'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MediaType = keyof typeof MEDIA_HKDF_KEY_MAPPING
|
export type MediaType = keyof typeof MEDIA_HKDF_KEY_MAPPING
|
||||||
|
|||||||
@@ -2258,6 +2258,17 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle view-once unavailable sync from primary device.
|
||||||
|
// When the primary device sends a view-once, linked companions receive
|
||||||
|
// <unavailable type="view_once"/> — no enc content, just a sync signal.
|
||||||
|
// Acknowledge and skip decryption (nothing to decrypt).
|
||||||
|
const unavailableNode = getBinaryNodeChild(node, 'unavailable')
|
||||||
|
if (unavailableNode?.attrs?.type === 'view_once') {
|
||||||
|
logger.debug({ id: node.attrs.id, from: node.attrs.from }, 'received view_once unavailable sync from primary device')
|
||||||
|
await sendMessageAck(node)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fullMessage: msg,
|
fullMessage: msg,
|
||||||
category,
|
category,
|
||||||
@@ -2552,6 +2563,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
|||||||
|
|
||||||
await sendReceipt(msg.key.remoteJid!, participant!, [msg.key.id!], type)
|
await sendReceipt(msg.key.remoteJid!, participant!, [msg.key.id!], type)
|
||||||
|
|
||||||
|
// NOTE: do NOT send view_once_read from a linked device (API).
|
||||||
|
// WA shows view-once as view_once_unavailable_fanout on linked devices — only the
|
||||||
|
// primary phone sends view_once_read when the user actually opens it.
|
||||||
|
// Sending it from a linked device causes WA to flag the account and disables view-once.
|
||||||
|
|
||||||
// send ack for history message
|
// send ack for history message
|
||||||
const isAnyHistoryMsg = getHistoryMsg(msg.message!)
|
const isAnyHistoryMsg = getHistoryMsg(msg.message!)
|
||||||
if (isAnyHistoryMsg) {
|
if (isAnyHistoryMsg) {
|
||||||
|
|||||||
@@ -1041,7 +1041,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await authState.keys.transaction(async () => {
|
await authState.keys.transaction(async () => {
|
||||||
const mediaType = getMediaType(message)
|
// normalizeMessageContent unwraps viewOnceMessage/viewOnceMessageV2 so getMediaType works correctly
|
||||||
|
const mediaType = getMediaType(normalizeMessageContent(message) || message)
|
||||||
if (mediaType) {
|
if (mediaType) {
|
||||||
extraAttrs['mediatype'] = mediaType
|
extraAttrs['mediatype'] = mediaType
|
||||||
}
|
}
|
||||||
@@ -1051,7 +1052,7 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
const bytes = encodeNewsletterMessage(patched as proto.IMessage)
|
const bytes = encodeNewsletterMessage(patched as proto.IMessage)
|
||||||
binaryNodeContent.push({
|
binaryNodeContent.push({
|
||||||
tag: 'plaintext',
|
tag: 'plaintext',
|
||||||
attrs: {},
|
attrs: mediaType ? { mediatype: mediaType } : {},
|
||||||
content: bytes
|
content: bytes
|
||||||
})
|
})
|
||||||
const stanza: BinaryNode = {
|
const stanza: BinaryNode = {
|
||||||
@@ -1073,6 +1074,8 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
extraAttrs['decrypt-fail'] = 'hide' // todo: expand for reactions and other types
|
extraAttrs['decrypt-fail'] = 'hide' // todo: expand for reactions and other types
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// view_once_write disabled — causes server to drop message (validation fails for linked device sessions)
|
||||||
|
|
||||||
if (isGroupOrStatus && !isRetryResend) {
|
if (isGroupOrStatus && !isRetryResend) {
|
||||||
const [groupData, senderKeyMap] = await Promise.all([
|
const [groupData, senderKeyMap] = await Promise.all([
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -1277,21 +1280,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
|
createParticipantNodes(viewOnceMeRecipients, meMsg || message, extraAttrs),
|
||||||
createParticipantNodes(effectiveMeRecipients, 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) {
|
// phash: para view-once inclui só celular principal + destinatários
|
||||||
extraAttrs['phash'] = generateParticipantHashV2(effectiveAllRecipients)
|
const phashRecipients = isViewOnceMsg
|
||||||
|
? [...viewOnceMeRecipients, ...effectiveOtherRecipients]
|
||||||
|
: effectiveAllRecipients
|
||||||
|
if (phashRecipients.length > 0) {
|
||||||
|
extraAttrs['phash'] = generateParticipantHashV2(phashRecipients)
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
|
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2
|
||||||
@@ -1897,6 +1927,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) {
|
||||||
|
|||||||
+13
-2
@@ -155,6 +155,7 @@ export type MessageReceiptType =
|
|||||||
| 'sender'
|
| 'sender'
|
||||||
| 'inactive'
|
| 'inactive'
|
||||||
| 'played'
|
| 'played'
|
||||||
|
| 'view_once_read'
|
||||||
| undefined
|
| undefined
|
||||||
|
|
||||||
export type MediaConnInfo = {
|
export type MediaConnInfo = {
|
||||||
@@ -178,6 +179,8 @@ export interface WAUrlInfo {
|
|||||||
type Mentionable = {
|
type Mentionable = {
|
||||||
/** list of jids that are mentioned in the accompanying text */
|
/** list of jids that are mentioned in the accompanying text */
|
||||||
mentions?: string[]
|
mentions?: string[]
|
||||||
|
/** mention all participants in the group */
|
||||||
|
mentionAll?: boolean
|
||||||
}
|
}
|
||||||
type Contextable = {
|
type Contextable = {
|
||||||
/** add contextInfo to the message */
|
/** add contextInfo to the message */
|
||||||
@@ -990,8 +993,16 @@ export type MessageGenerationOptionsFromContent = MiscMessageGenerationOptions &
|
|||||||
|
|
||||||
export type WAMediaUploadFunction = (
|
export type WAMediaUploadFunction = (
|
||||||
encFilePath: string,
|
encFilePath: string,
|
||||||
opts: { fileEncSha256B64: string; mediaType: MediaType; timeoutMs?: number }
|
opts: { fileEncSha256B64: string; mediaType: MediaType; timeoutMs?: number; newsletter?: boolean }
|
||||||
) => Promise<{ mediaUrl: string; directPath: string; meta_hmac?: string; ts?: number; fbid?: number }>
|
) => Promise<{
|
||||||
|
mediaUrl: string | undefined
|
||||||
|
directPath: string
|
||||||
|
meta_hmac?: string
|
||||||
|
ts?: number
|
||||||
|
fbid?: number
|
||||||
|
thumbnailDirectPath?: string
|
||||||
|
thumbnailSha256?: string
|
||||||
|
}>
|
||||||
|
|
||||||
export type MediaGenerationOptions = {
|
export type MediaGenerationOptions = {
|
||||||
logger?: ILogger
|
logger?: ILogger
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ import { join } from 'path'
|
|||||||
import { Readable, Transform } from 'stream'
|
import { Readable, Transform } from 'stream'
|
||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
import { proto } from '../../WAProto/index.js'
|
import { proto } from '../../WAProto/index.js'
|
||||||
import { DEFAULT_ORIGIN, MEDIA_HKDF_KEY_MAPPING, MEDIA_PATH_MAP, type MediaType } from '../Defaults'
|
import {
|
||||||
|
DEFAULT_ORIGIN,
|
||||||
|
MEDIA_HKDF_KEY_MAPPING,
|
||||||
|
MEDIA_PATH_MAP,
|
||||||
|
type MediaType,
|
||||||
|
NEWSLETTER_MEDIA_PATH_MAP
|
||||||
|
} from '../Defaults'
|
||||||
import type {
|
import type {
|
||||||
BaileysEventMap,
|
BaileysEventMap,
|
||||||
DownloadableMessage,
|
DownloadableMessage,
|
||||||
@@ -681,6 +687,10 @@ type MediaUploadResult = {
|
|||||||
meta_hmac?: string
|
meta_hmac?: string
|
||||||
ts?: number
|
ts?: number
|
||||||
fbid?: number
|
fbid?: number
|
||||||
|
thumbnail_info?: {
|
||||||
|
thumbnail_sha256?: string
|
||||||
|
thumbnail_direct_path?: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UploadParams = {
|
export type UploadParams = {
|
||||||
@@ -825,11 +835,21 @@ export const getWAUploadToServer = (
|
|||||||
{ customUploadHosts, fetchAgent, logger, options }: SocketConfig,
|
{ customUploadHosts, fetchAgent, logger, options }: SocketConfig,
|
||||||
refreshMediaConn: (force: boolean) => Promise<MediaConnInfo>
|
refreshMediaConn: (force: boolean) => Promise<MediaConnInfo>
|
||||||
): WAMediaUploadFunction => {
|
): WAMediaUploadFunction => {
|
||||||
return async (filePath, { mediaType, fileEncSha256B64, timeoutMs }) => {
|
return async (filePath, { mediaType, fileEncSha256B64, timeoutMs, newsletter }) => {
|
||||||
// send a query JSON to obtain the url & auth token to upload our media
|
// send a query JSON to obtain the url & auth token to upload our media
|
||||||
let uploadInfo = await refreshMediaConn(false)
|
let uploadInfo = await refreshMediaConn(false)
|
||||||
|
|
||||||
let urls: { mediaUrl: string; directPath: string; meta_hmac?: string; ts?: number; fbid?: number } | undefined
|
let urls:
|
||||||
|
| {
|
||||||
|
mediaUrl: string
|
||||||
|
directPath: string
|
||||||
|
meta_hmac?: string
|
||||||
|
ts?: number
|
||||||
|
fbid?: number
|
||||||
|
thumbnailDirectPath?: string
|
||||||
|
thumbnailSha256?: string
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
const hosts = [...customUploadHosts, ...uploadInfo.hosts]
|
const hosts = [...customUploadHosts, ...uploadInfo.hosts]
|
||||||
|
|
||||||
fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
|
fileEncSha256B64 = encodeBase64EncodedStringForUpload(fileEncSha256B64)
|
||||||
@@ -851,7 +871,11 @@ export const getWAUploadToServer = (
|
|||||||
logger.debug(`uploading to "${hostname}"`)
|
logger.debug(`uploading to "${hostname}"`)
|
||||||
|
|
||||||
const auth = encodeURIComponent(uploadInfo.auth)
|
const auth = encodeURIComponent(uploadInfo.auth)
|
||||||
const url = `https://${hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
|
const mediaPath = (newsletter ? NEWSLETTER_MEDIA_PATH_MAP[mediaType] : undefined) || MEDIA_PATH_MAP[mediaType]
|
||||||
|
let url = `https://${hostname}${mediaPath}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
|
||||||
|
if (newsletter) {
|
||||||
|
url += '&server_thumb_gen=1'
|
||||||
|
}
|
||||||
|
|
||||||
let result: MediaUploadResult | undefined
|
let result: MediaUploadResult | undefined
|
||||||
try {
|
try {
|
||||||
@@ -868,11 +892,13 @@ export const getWAUploadToServer = (
|
|||||||
|
|
||||||
if (result?.url || result?.direct_path) {
|
if (result?.url || result?.direct_path) {
|
||||||
urls = {
|
urls = {
|
||||||
mediaUrl: result.url!,
|
mediaUrl: result.url || result.direct_path!,
|
||||||
directPath: result.direct_path!,
|
directPath: result.direct_path!,
|
||||||
meta_hmac: result.meta_hmac,
|
meta_hmac: result.meta_hmac,
|
||||||
fbid: result.fbid,
|
fbid: result.fbid,
|
||||||
ts: result.ts
|
ts: result.ts,
|
||||||
|
thumbnailDirectPath: result.thumbnail_info?.thumbnail_direct_path,
|
||||||
|
thumbnailSha256: result.thumbnail_info?.thumbnail_sha256
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+46
-14
@@ -196,10 +196,11 @@ export const prepareWAMessageMedia = async (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const fileSha256B64 = fileSha256.toString('base64')
|
const fileSha256B64 = fileSha256.toString('base64')
|
||||||
const { mediaUrl, directPath } = await options.upload(filePath, {
|
const { directPath, thumbnailDirectPath, thumbnailSha256 } = await options.upload(filePath, {
|
||||||
fileEncSha256B64: fileSha256B64,
|
fileEncSha256B64: fileSha256B64,
|
||||||
mediaType: mediaType,
|
mediaType: mediaType,
|
||||||
timeoutMs: options.mediaUploadTimeoutMs
|
timeoutMs: options.mediaUploadTimeoutMs,
|
||||||
|
newsletter: true
|
||||||
})
|
})
|
||||||
|
|
||||||
await fs.unlink(filePath)
|
await fs.unlink(filePath)
|
||||||
@@ -207,10 +208,12 @@ export const prepareWAMessageMedia = async (
|
|||||||
const obj = WAProto.Message.fromObject({
|
const obj = WAProto.Message.fromObject({
|
||||||
// todo: add more support here
|
// todo: add more support here
|
||||||
[`${mediaType}Message`]: (MessageTypeProto as any)[mediaType].fromObject({
|
[`${mediaType}Message`]: (MessageTypeProto as any)[mediaType].fromObject({
|
||||||
url: mediaUrl,
|
// url intentionally omitted — newsletters use directPath only
|
||||||
directPath,
|
directPath,
|
||||||
fileSha256,
|
fileSha256,
|
||||||
fileLength,
|
fileLength,
|
||||||
|
thumbnailDirectPath,
|
||||||
|
thumbnailSha256: thumbnailSha256 ? Buffer.from(thumbnailSha256, 'base64') : undefined,
|
||||||
...uploadData,
|
...uploadData,
|
||||||
media: undefined
|
media: undefined
|
||||||
})
|
})
|
||||||
@@ -236,7 +239,7 @@ export const prepareWAMessageMedia = async (
|
|||||||
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
|
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
|
||||||
const requiresThumbnailComputation =
|
const requiresThumbnailComputation =
|
||||||
(mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined'
|
(mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined'
|
||||||
const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true
|
const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true && typeof uploadData.waveform === 'undefined'
|
||||||
const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true
|
const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true
|
||||||
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation
|
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation
|
||||||
const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(
|
const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(
|
||||||
@@ -254,7 +257,9 @@ export const prepareWAMessageMedia = async (
|
|||||||
(async () => {
|
(async () => {
|
||||||
const result = await options.upload(encFilePath, {
|
const result = await options.upload(encFilePath, {
|
||||||
fileEncSha256B64,
|
fileEncSha256B64,
|
||||||
mediaType,
|
// Use mediaTypeOverride for upload path (e.g. 'viewonce-image' → /o1/mms/image)
|
||||||
|
// Encryption already used the override via encryptedStream above
|
||||||
|
mediaType: options.mediaTypeOverride || mediaType,
|
||||||
timeoutMs: options.mediaUploadTimeoutMs
|
timeoutMs: options.mediaUploadTimeoutMs
|
||||||
})
|
})
|
||||||
logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
|
logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
|
||||||
@@ -292,7 +297,7 @@ export const prepareWAMessageMedia = async (
|
|||||||
logger?.debug('computed audio duration')
|
logger?.debug('computed audio duration')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requiresWaveformProcessing) {
|
if (requiresWaveformProcessing && !uploadData.waveform) {
|
||||||
uploadData.waveform = await getAudioWaveform(filePath, logger)
|
uploadData.waveform = await getAudioWaveform(filePath, logger)
|
||||||
logger?.debug('processed waveform')
|
logger?.debug('processed waveform')
|
||||||
}
|
}
|
||||||
@@ -1747,22 +1752,49 @@ export const generateWAMessageContent = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m = await prepareWAMessageMedia(message, options)
|
// For view-once media, upload to the one-time CDN (/o1/mms/*) instead of regular CDN
|
||||||
|
// WA Web view-once messages always have directPath: /o1/v/t24/... (not /v/t62.7118-24/...)
|
||||||
|
// Using the wrong CDN causes WA server to flag the account and breaks view-once capability
|
||||||
|
const isViewOnce = hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce
|
||||||
|
const viewOnceTypeMap: Record<string, string> = {
|
||||||
|
image: 'viewonce-image',
|
||||||
|
video: 'viewonce-video',
|
||||||
|
audio: 'viewonce-audio'
|
||||||
|
}
|
||||||
|
const viewOnceOverride = isViewOnce
|
||||||
|
? viewOnceTypeMap[Object.keys(message).find(k => k in viewOnceTypeMap) || '']
|
||||||
|
: undefined
|
||||||
|
m = await prepareWAMessageMedia(message, {
|
||||||
|
...options,
|
||||||
|
mediaTypeOverride: (viewOnceOverride as any) || options.mediaTypeOverride
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
|
if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
|
||||||
m = { viewOnceMessage: { message: m } }
|
// Also set viewOnce=true on the inner media message (required by WA app to render as view-once)
|
||||||
|
if (m.imageMessage) m.imageMessage.viewOnce = true
|
||||||
|
else if (m.videoMessage) m.videoMessage.viewOnce = true
|
||||||
|
else if (m.audioMessage) m.audioMessage.viewOnce = true
|
||||||
|
// Use viewOnceMessageV2 (field 55) — modern WA clients use this wrapper
|
||||||
|
// viewOnceMessage (field 37) is the old format which WA server no longer accepts with view_once_write
|
||||||
|
m = { viewOnceMessageV2: { message: m } }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasOptionalProperty(message, 'mentions') && message.mentions?.length) {
|
if (
|
||||||
|
(hasOptionalProperty(message, 'mentions') && message.mentions?.length) ||
|
||||||
|
(hasOptionalProperty(message, 'mentionAll') && message.mentionAll)
|
||||||
|
) {
|
||||||
const messageType = Object.keys(m)[0] as Extract<keyof proto.IMessage, MessageWithContextInfo>
|
const messageType = Object.keys(m)[0] as Extract<keyof proto.IMessage, MessageWithContextInfo>
|
||||||
if (messageType) {
|
if (messageType) {
|
||||||
const key = m[messageType]
|
const key = m[messageType]
|
||||||
if (key && 'contextInfo' in key && !!key.contextInfo) {
|
if (key && 'contextInfo' in key) {
|
||||||
key.contextInfo.mentionedJid = message.mentions
|
key.contextInfo = key.contextInfo || {}
|
||||||
} else if (key) {
|
if (message.mentions?.length) {
|
||||||
key.contextInfo = {
|
key.contextInfo.mentionedJid = message.mentions
|
||||||
mentionedJid: message.mentions
|
}
|
||||||
|
|
||||||
|
if (message.mentionAll) {
|
||||||
|
key.contextInfo.nonJidMentions = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user