This commit is contained in:
tzagim 2024-05-21 20:08:12 +03:00 committed by GitHub
parent ff6d9f23e3
commit 0cf7c09ed5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 31569 additions and 479 deletions

View File

@ -223,6 +223,41 @@ func (cli *Client) dispatchAppState(mutation appstate.Mutation, fullSync bool, e
Action: mutation.Action.GetUserStatusMuteAction(),
FromFullSync: fullSync,
}
case appstate.IndexLabelEdit:
act := mutation.Action.GetLabelEditAction()
eventToDispatch = &events.LabelEdit{
Timestamp: ts,
LabelID: mutation.Index[1],
Action: act,
FromFullSync: fullSync,
}
case appstate.IndexLabelAssociationChat:
if len(mutation.Index) < 3 {
return
}
jid, _ = types.ParseJID(mutation.Index[2])
act := mutation.Action.GetLabelAssociationAction()
eventToDispatch = &events.LabelAssociationChat{
JID: jid,
Timestamp: ts,
LabelID: mutation.Index[1],
Action: act,
FromFullSync: fullSync,
}
case appstate.IndexLabelAssociationMessage:
if len(mutation.Index) < 6 {
return
}
jid, _ = types.ParseJID(mutation.Index[2])
act := mutation.Action.GetLabelAssociationAction()
eventToDispatch = &events.LabelAssociationMessage{
JID: jid,
Timestamp: ts,
LabelID: mutation.Index[1],
MessageID: mutation.Index[3],
Action: act,
FromFullSync: fullSync,
}
}
if storeUpdateError != nil {
cli.Log.Errorf("Failed to update device store after app state mutation: %v", storeUpdateError)

View File

@ -0,0 +1,99 @@
// Copyright (c) 2024 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"fmt"
"google.golang.org/protobuf/proto"
"go.mau.fi/whatsmeow/binary/armadillo"
"go.mau.fi/whatsmeow/binary/armadillo/waCommon"
"go.mau.fi/whatsmeow/binary/armadillo/waMsgApplication"
"go.mau.fi/whatsmeow/binary/armadillo/waMsgTransport"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
func (cli *Client) handleDecryptedArmadillo(info *types.MessageInfo, decrypted []byte, retryCount int) bool {
dec, err := decodeArmadillo(decrypted)
if err != nil {
cli.Log.Warnf("Failed to decode armadillo message from %s: %v", info.SourceString(), err)
return false
}
dec.Info = *info
dec.RetryCount = retryCount
if dec.Transport.GetProtocol().GetAncillary().GetSkdm() != nil {
if !info.IsGroup {
cli.Log.Warnf("Got sender key distribution message in non-group chat from %s", info.Sender)
} else {
skdm := dec.Transport.GetProtocol().GetAncillary().GetSkdm()
cli.handleSenderKeyDistributionMessage(info.Chat, info.Sender, skdm.AxolotlSenderKeyDistributionMessage)
}
}
if dec.Message != nil {
cli.dispatchEvent(&dec)
}
return true
}
func decodeArmadillo(data []byte) (dec events.FBMessage, err error) {
var transport waMsgTransport.MessageTransport
err = proto.Unmarshal(data, &transport)
if err != nil {
return dec, fmt.Errorf("failed to unmarshal transport: %w", err)
}
dec.Transport = &transport
if transport.GetPayload() == nil {
return
}
application, err := transport.GetPayload().Decode()
if err != nil {
return dec, fmt.Errorf("failed to unmarshal application: %w", err)
}
dec.Application = application
if application.GetPayload() == nil {
return
}
switch typedContent := application.GetPayload().GetContent().(type) {
case *waMsgApplication.MessageApplication_Payload_CoreContent:
err = fmt.Errorf("unsupported core content payload")
case *waMsgApplication.MessageApplication_Payload_Signal:
err = fmt.Errorf("unsupported signal payload")
case *waMsgApplication.MessageApplication_Payload_ApplicationData:
err = fmt.Errorf("unsupported application data payload")
case *waMsgApplication.MessageApplication_Payload_SubProtocol:
var protoMsg proto.Message
var subData *waCommon.SubProtocol
switch subProtocol := typedContent.SubProtocol.GetSubProtocol().(type) {
case *waMsgApplication.MessageApplication_SubProtocolPayload_ConsumerMessage:
dec.Message, err = subProtocol.Decode()
case *waMsgApplication.MessageApplication_SubProtocolPayload_BusinessMessage:
dec.Message = (*armadillo.Unsupported_BusinessApplication)(subProtocol.BusinessMessage)
case *waMsgApplication.MessageApplication_SubProtocolPayload_PaymentMessage:
dec.Message = (*armadillo.Unsupported_PaymentApplication)(subProtocol.PaymentMessage)
case *waMsgApplication.MessageApplication_SubProtocolPayload_MultiDevice:
dec.Message, err = subProtocol.Decode()
case *waMsgApplication.MessageApplication_SubProtocolPayload_Voip:
dec.Message = (*armadillo.Unsupported_Voip)(subProtocol.Voip)
case *waMsgApplication.MessageApplication_SubProtocolPayload_Armadillo:
dec.Message, err = subProtocol.Decode()
default:
return dec, fmt.Errorf("unsupported subprotocol type: %T", subProtocol)
}
if protoMsg != nil {
err = proto.Unmarshal(subData.GetPayload(), protoMsg)
if err != nil {
return dec, fmt.Errorf("failed to unmarshal application subprotocol payload (%T v%d): %w", protoMsg, subData.GetVersion(), err)
}
}
default:
err = fmt.Errorf("unsupported application payload content type: %T", typedContent)
}
return
}

View File

@ -0,0 +1,32 @@
package armadilloutil
import (
"errors"
"fmt"
"google.golang.org/protobuf/proto"
"go.mau.fi/whatsmeow/binary/armadillo/waCommon"
)
var ErrUnsupportedVersion = errors.New("unsupported subprotocol version")
func Unmarshal[T proto.Message](into T, msg *waCommon.SubProtocol, expectedVersion int32) (T, error) {
if msg.GetVersion() != expectedVersion {
return into, fmt.Errorf("%w %d in %T (expected %d)", ErrUnsupportedVersion, msg.GetVersion(), into, expectedVersion)
}
err := proto.Unmarshal(msg.GetPayload(), into)
return into, err
}
func Marshal[T proto.Message](msg T, version int32) (*waCommon.SubProtocol, error) {
payload, err := proto.Marshal(msg)
if err != nil {
return nil, err
}
return &waCommon.SubProtocol{
Payload: payload,
Version: version,
}, nil
}

View File

@ -0,0 +1,29 @@
package armadillo
import (
"go.mau.fi/whatsmeow/binary/armadillo/waArmadilloApplication"
"go.mau.fi/whatsmeow/binary/armadillo/waCommon"
"go.mau.fi/whatsmeow/binary/armadillo/waConsumerApplication"
"go.mau.fi/whatsmeow/binary/armadillo/waMultiDevice"
)
type MessageApplicationSub interface {
IsMessageApplicationSub()
}
type Unsupported_BusinessApplication waCommon.SubProtocol
type Unsupported_PaymentApplication waCommon.SubProtocol
type Unsupported_Voip waCommon.SubProtocol
var (
_ MessageApplicationSub = (*waConsumerApplication.ConsumerApplication)(nil) // 2
_ MessageApplicationSub = (*Unsupported_BusinessApplication)(nil) // 3
_ MessageApplicationSub = (*Unsupported_PaymentApplication)(nil) // 4
_ MessageApplicationSub = (*waMultiDevice.MultiDevice)(nil) // 5
_ MessageApplicationSub = (*Unsupported_Voip)(nil) // 6
_ MessageApplicationSub = (*waArmadilloApplication.Armadillo)(nil) // 7
)
func (*Unsupported_BusinessApplication) IsMessageApplicationSub() {}
func (*Unsupported_PaymentApplication) IsMessageApplicationSub() {}
func (*Unsupported_Voip) IsMessageApplicationSub() {}

View File

@ -0,0 +1,9 @@
#!/bin/bash
cd $(dirname $0)
set -euo pipefail
if [[ ! -f "e2ee.js" ]]; then
echo "Please download the encryption javascript file and save it to e2ee.js first"
exit 1
fi
node parse-proto.js
protoc --go_out=. --go_opt=paths=source_relative --go_opt=embed_raw=true */*.proto

View File

@ -0,0 +1,351 @@
///////////////////
// JS EVALUATION //
///////////////////
const protos = []
const modules = {
"$InternalEnum": {
exports: {
exports: function (data) {
data.__enum__ = true
return data
}
}
},
}
function requireModule(name) {
if (!modules[name]) {
throw new Error(`Unknown requirement ${name}`)
}
return modules[name].exports
}
function requireDefault(name) {
return requireModule(name).exports
}
function ignoreModule(name) {
if (name === "WAProtoConst") {
return false
} else if (!name.endsWith(".pb")) {
// Ignore any non-protobuf modules, except WAProtoConst above
return true
} else if (name.startsWith("MAWArmadillo") && (name.endsWith("TableSchema.pb") || name.endsWith("TablesSchema.pb"))) {
// Ignore internal table schemas
return true
} else if (name === "WASignalLocalStorageProtocol.pb" || name === "WASignalWhisperTextProtocol.pb") {
// Ignore standard signal protocol stuff
return true
} else {
return false
}
}
function defineModule(name, dependencies, callback, unknownIntOrNull) {
if (ignoreModule(name)) {
return
}
const exports = {}
if (dependencies.length > 0) {
callback(null, requireDefault, null, requireModule, null, null, exports)
} else {
callback(null, requireDefault, null, requireModule, exports, exports)
}
modules[name] = {exports, dependencies}
}
global.self = global
global.__d = defineModule
require("./e2ee.js")
function dereference(obj, module, currentPath, next, ...remainder) {
if (!next) {
return obj
}
if (!obj.messages[next]) {
obj.messages[next] = {messages: {}, enums: {}, __module__: module, __path__: currentPath, __name__: next}
}
return dereference(obj.messages[next], module, currentPath.concat([next]), ...remainder)
}
function dereferenceSnake(obj, currentPath, path) {
let next = path[0]
path = path.slice(1)
while (!obj.messages[next]) {
if (path.length === 0) {
return [obj, currentPath, next]
}
next += path[0]
path = path.slice(1)
}
return dereferenceSnake(obj.messages[next], currentPath.concat([next]), path)
}
function renameModule(name) {
return name.replace(".pb", "")
}
function renameDependencies(dependencies) {
return dependencies
.filter(name => name.endsWith(".pb"))
.map(renameModule)
.map(name => name === "WAProtocol" ? "WACommon" : name)
}
function renameType(protoName, fieldName, field) {
return fieldName
}
for (const [name, module] of Object.entries(modules)) {
if (!name.endsWith(".pb")) {
continue
} else if (!module.exports) {
console.warn(name, "has no exports")
continue
}
// Slightly hacky way to get rid of WAProtocol.pb and just use the MessageKey in WACommon
if (name === "WAProtocol.pb") {
if (Object.entries(module.exports).length > 1) {
console.warn("WAProtocol.pb has more than one export")
}
module.exports["MessageKeySpec"].__name__ = "MessageKey"
module.exports["MessageKeySpec"].__module__ = "WACommon"
module.exports["MessageKeySpec"].__path__ = []
continue
}
const proto = {
__protobuf__: true,
messages: {},
enums: {},
__name__: renameModule(name),
dependencies: renameDependencies(module.dependencies),
}
const upperSnakeEnums = []
for (const [name, field] of Object.entries(module.exports)) {
const namePath = name.replace(/Spec$/, "").split("$")
field.__name__ = renameType(proto.__name__, namePath[namePath.length - 1], field)
namePath[namePath.length - 1] = field.__name__
field.__path__ = namePath.slice(0, -1)
field.__module__ = proto.__name__
if (field.internalSpec) {
dereference(proto, proto.__name__, [], ...namePath).message = field.internalSpec
} else if (namePath.length === 1 && name.toUpperCase() === name) {
upperSnakeEnums.push(field)
} else {
dereference(proto, proto.__name__, [], ...namePath.slice(0, -1)).enums[field.__name__] = field
}
}
// Some enums have uppercase names, instead of capital case with $ separators.
// For those, we need to find the right nesting location.
for (const field of upperSnakeEnums) {
field.__enum__ = true
const [obj, path, name] = dereferenceSnake(proto, [], field.__name__.split("_").map(part => part[0] + part.slice(1).toLowerCase()))
field.__path__ = path
field.__name__ = name
field.__module__ = proto.__name__
obj.enums[name] = field
}
protos.push(proto)
}
////////////////////////////////
// PROTOBUF SCHEMA GENERATION //
////////////////////////////////
function indent(lines, indent = "\t") {
return lines.map(line => line ? `${indent}${line}` : "")
}
function flattenWithBlankLines(...items) {
return items
.flatMap(item => item.length > 0 ? [item, [""]] : [])
.slice(0, -1)
.flatMap(item => item)
}
function protoifyChildren(container) {
return flattenWithBlankLines(
...Object.values(container.enums).map(protoifyEnum),
...Object.values(container.messages).map(protoifyMessage),
)
}
function protoifyEnum(enumDef) {
const values = []
const names = Object.fromEntries(Object.entries(enumDef).map(([name, value]) => [value, name]))
if (!names["0"]) {
if (names["-1"]) {
enumDef[names["-1"]] = 0
} else {
// TODO add snake case
values.push(`${enumDef.__name__.toUpperCase()}_UNKNOWN = 0;`)
}
}
for (const [name, value] of Object.entries(enumDef)) {
if (name.startsWith("__") && name.endsWith("__")) {
continue
}
values.push(`${name} = ${value};`)
}
return [`enum ${enumDef.__name__} ` + "{", ...indent(values), "}"]
}
const {TYPES, TYPE_MASK, FLAGS} = requireModule("WAProtoConst")
function fieldTypeName(typeID, typeRef, parentModule, parentPath) {
switch (typeID) {
case TYPES.INT32:
return "int32"
case TYPES.INT64:
return "int64"
case TYPES.UINT32:
return "uint32"
case TYPES.UINT64:
return "uint64"
case TYPES.SINT32:
return "sint32"
case TYPES.SINT64:
return "sint64"
case TYPES.BOOL:
return "bool"
case TYPES.ENUM:
case TYPES.MESSAGE:
let pathStartIndex = 0
for (let i = 0; i < parentPath.length && i < typeRef.__path__.length; i++) {
if (typeRef.__path__[i] === parentPath[i]) {
pathStartIndex++
} else {
break
}
}
const namePath = []
if (typeRef.__module__ !== parentModule) {
namePath.push(typeRef.__module__)
pathStartIndex = 0
}
namePath.push(...typeRef.__path__.slice(pathStartIndex))
namePath.push(typeRef.__name__)
return namePath.join(".")
case TYPES.FIXED64:
return "fixed64"
case TYPES.SFIXED64:
return "sfixed64"
case TYPES.DOUBLE:
return "double"
case TYPES.STRING:
return "string"
case TYPES.BYTES:
return "bytes"
case TYPES.FIXED32:
return "fixed32"
case TYPES.SFIXED32:
return "sfixed32"
case TYPES.FLOAT:
return "float"
}
}
const staticRenames = {
id: "ID",
jid: "JID",
encIv: "encIV",
iv: "IV",
ptt: "PTT",
hmac: "HMAC",
url: "URL",
fbid: "FBID",
jpegThumbnail: "JPEGThumbnail",
dsm: "DSM",
}
function fixFieldName(name) {
if (name === "id") {
return "ID"
} else if (name === "encIv") {
return "encIV"
}
return staticRenames[name] ?? name
.replace(/Id([A-Zs]|$)/, "ID$1")
.replace("Jid", "JID")
.replace(/Ms([A-Z]|$)/, "MS$1")
.replace(/Ts([A-Z]|$)/, "TS$1")
.replace(/Mac([A-Z]|$)/, "MAC$1")
.replace("Url", "URL")
.replace("Cdn", "CDN")
.replace("Json", "JSON")
.replace("Jpeg", "JPEG")
.replace("Sha256", "SHA256")
}
function protoifyField(name, [index, flags, typeRef], parentModule, parentPath) {
const preflags = []
const postflags = [""]
if ((flags & FLAGS.REPEATED) !== 0) {
preflags.push("repeated")
}
// if ((flags & FLAGS.REQUIRED) === 0) {
// preflags.push("optional")
// } else {
// preflags.push("required")
// }
preflags.push(fieldTypeName(flags & TYPE_MASK, typeRef, parentModule, parentPath))
if ((flags & FLAGS.PACKED) !== 0) {
postflags.push(`[packed=true]`)
}
return `${preflags.join(" ")} ${fixFieldName(name)} = ${index}${postflags.join(" ")};`
}
function protoifyFields(fields, parentModule, parentPath) {
return Object.entries(fields).map(([name, definition]) => protoifyField(name, definition, parentModule, parentPath))
}
function protoifyMessage(message) {
const sections = [protoifyChildren(message)]
const spec = message.message
const fullMessagePath = message.__path__.concat([message.__name__])
for (const [name, fieldNames] of Object.entries(spec.__oneofs__ ?? {})) {
const fields = Object.fromEntries(fieldNames.map(fieldName => {
const def = spec[fieldName]
delete spec[fieldName]
return [fieldName, def]
}))
sections.push([`oneof ${name} ` + "{", ...indent(protoifyFields(fields, message.__module__, fullMessagePath)), "}"])
}
if (spec.__reserved__) {
console.warn("Found reserved keys:", message.__name__, spec.__reserved__)
}
delete spec.__oneofs__
delete spec.__reserved__
sections.push(protoifyFields(spec, message.__module__, fullMessagePath))
return [`message ${message.__name__} ` + "{", ...indent(flattenWithBlankLines(...sections)), "}"]
}
function goPackageName(name) {
return name.replace(/^WA/, "wa")
}
function protoifyModule(module) {
const output = []
output.push(`syntax = "proto3";`)
output.push(`package ${module.__name__};`)
output.push(`option go_package = "go.mau.fi/whatsmeow/binary/armadillo/${goPackageName(module.__name__)}";`)
output.push("")
if (module.dependencies.length > 0) {
for (const dependency of module.dependencies) {
output.push(`import "${goPackageName(dependency)}/${dependency}.proto";`)
}
output.push("")
}
const children = protoifyChildren(module)
children.push("")
return output.concat(children)
}
const fs = require("fs")
for (const proto of protos) {
fs.mkdirSync(goPackageName(proto.__name__), {recursive: true})
fs.writeFileSync(`${goPackageName(proto.__name__)}/${proto.__name__}.proto`, protoifyModule(proto).join("\n"))
}

View File

@ -0,0 +1,552 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc v3.21.12
// source: waAdv/WAAdv.proto
package waAdv
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
_ "embed"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ADVEncryptionType int32
const (
ADVEncryptionType_E2EE ADVEncryptionType = 0
ADVEncryptionType_HOSTED ADVEncryptionType = 1
)
// Enum value maps for ADVEncryptionType.
var (
ADVEncryptionType_name = map[int32]string{
0: "E2EE",
1: "HOSTED",
}
ADVEncryptionType_value = map[string]int32{
"E2EE": 0,
"HOSTED": 1,
}
)
func (x ADVEncryptionType) Enum() *ADVEncryptionType {
p := new(ADVEncryptionType)
*p = x
return p
}
func (x ADVEncryptionType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ADVEncryptionType) Descriptor() protoreflect.EnumDescriptor {
return file_waAdv_WAAdv_proto_enumTypes[0].Descriptor()
}
func (ADVEncryptionType) Type() protoreflect.EnumType {
return &file_waAdv_WAAdv_proto_enumTypes[0]
}
func (x ADVEncryptionType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ADVEncryptionType.Descriptor instead.
func (ADVEncryptionType) EnumDescriptor() ([]byte, []int) {
return file_waAdv_WAAdv_proto_rawDescGZIP(), []int{0}
}
type ADVKeyIndexList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RawID uint32 `protobuf:"varint,1,opt,name=rawID,proto3" json:"rawID,omitempty"`
Timestamp uint64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
CurrentIndex uint32 `protobuf:"varint,3,opt,name=currentIndex,proto3" json:"currentIndex,omitempty"`
ValidIndexes []uint32 `protobuf:"varint,4,rep,packed,name=validIndexes,proto3" json:"validIndexes,omitempty"`
AccountType ADVEncryptionType `protobuf:"varint,5,opt,name=accountType,proto3,enum=WAAdv.ADVEncryptionType" json:"accountType,omitempty"`
}
func (x *ADVKeyIndexList) Reset() {
*x = ADVKeyIndexList{}
if protoimpl.UnsafeEnabled {
mi := &file_waAdv_WAAdv_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ADVKeyIndexList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ADVKeyIndexList) ProtoMessage() {}
func (x *ADVKeyIndexList) ProtoReflect() protoreflect.Message {
mi := &file_waAdv_WAAdv_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ADVKeyIndexList.ProtoReflect.Descriptor instead.
func (*ADVKeyIndexList) Descriptor() ([]byte, []int) {
return file_waAdv_WAAdv_proto_rawDescGZIP(), []int{0}
}
func (x *ADVKeyIndexList) GetRawID() uint32 {
if x != nil {
return x.RawID
}
return 0
}
func (x *ADVKeyIndexList) GetTimestamp() uint64 {
if x != nil {
return x.Timestamp
}
return 0
}
func (x *ADVKeyIndexList) GetCurrentIndex() uint32 {
if x != nil {
return x.CurrentIndex
}
return 0
}
func (x *ADVKeyIndexList) GetValidIndexes() []uint32 {
if x != nil {
return x.ValidIndexes
}
return nil
}
func (x *ADVKeyIndexList) GetAccountType() ADVEncryptionType {
if x != nil {
return x.AccountType
}
return ADVEncryptionType_E2EE
}
type ADVSignedKeyIndexList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Details []byte `protobuf:"bytes,1,opt,name=details,proto3" json:"details,omitempty"`
AccountSignature []byte `protobuf:"bytes,2,opt,name=accountSignature,proto3" json:"accountSignature,omitempty"`
AccountSignatureKey []byte `protobuf:"bytes,3,opt,name=accountSignatureKey,proto3" json:"accountSignatureKey,omitempty"`
}
func (x *ADVSignedKeyIndexList) Reset() {
*x = ADVSignedKeyIndexList{}
if protoimpl.UnsafeEnabled {
mi := &file_waAdv_WAAdv_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ADVSignedKeyIndexList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ADVSignedKeyIndexList) ProtoMessage() {}
func (x *ADVSignedKeyIndexList) ProtoReflect() protoreflect.Message {
mi := &file_waAdv_WAAdv_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ADVSignedKeyIndexList.ProtoReflect.Descriptor instead.
func (*ADVSignedKeyIndexList) Descriptor() ([]byte, []int) {
return file_waAdv_WAAdv_proto_rawDescGZIP(), []int{1}
}
func (x *ADVSignedKeyIndexList) GetDetails() []byte {
if x != nil {
return x.Details
}
return nil
}
func (x *ADVSignedKeyIndexList) GetAccountSignature() []byte {
if x != nil {
return x.AccountSignature
}
return nil
}
func (x *ADVSignedKeyIndexList) GetAccountSignatureKey() []byte {
if x != nil {
return x.AccountSignatureKey
}
return nil
}
type ADVDeviceIdentity struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RawID uint32 `protobuf:"varint,1,opt,name=rawID,proto3" json:"rawID,omitempty"`
Timestamp uint64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
KeyIndex uint32 `protobuf:"varint,3,opt,name=keyIndex,proto3" json:"keyIndex,omitempty"`
AccountType ADVEncryptionType `protobuf:"varint,4,opt,name=accountType,proto3,enum=WAAdv.ADVEncryptionType" json:"accountType,omitempty"`
DeviceType ADVEncryptionType `protobuf:"varint,5,opt,name=deviceType,proto3,enum=WAAdv.ADVEncryptionType" json:"deviceType,omitempty"`
}
func (x *ADVDeviceIdentity) Reset() {
*x = ADVDeviceIdentity{}
if protoimpl.UnsafeEnabled {
mi := &file_waAdv_WAAdv_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ADVDeviceIdentity) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ADVDeviceIdentity) ProtoMessage() {}
func (x *ADVDeviceIdentity) ProtoReflect() protoreflect.Message {
mi := &file_waAdv_WAAdv_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ADVDeviceIdentity.ProtoReflect.Descriptor instead.
func (*ADVDeviceIdentity) Descriptor() ([]byte, []int) {
return file_waAdv_WAAdv_proto_rawDescGZIP(), []int{2}
}
func (x *ADVDeviceIdentity) GetRawID() uint32 {
if x != nil {
return x.RawID
}
return 0
}
func (x *ADVDeviceIdentity) GetTimestamp() uint64 {
if x != nil {
return x.Timestamp
}
return 0
}
func (x *ADVDeviceIdentity) GetKeyIndex() uint32 {
if x != nil {
return x.KeyIndex
}
return 0
}
func (x *ADVDeviceIdentity) GetAccountType() ADVEncryptionType {
if x != nil {
return x.AccountType
}
return ADVEncryptionType_E2EE
}
func (x *ADVDeviceIdentity) GetDeviceType() ADVEncryptionType {
if x != nil {
return x.DeviceType
}
return ADVEncryptionType_E2EE
}
type ADVSignedDeviceIdentity struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Details []byte `protobuf:"bytes,1,opt,name=details,proto3" json:"details,omitempty"`
AccountSignatureKey []byte `protobuf:"bytes,2,opt,name=accountSignatureKey,proto3" json:"accountSignatureKey,omitempty"`
AccountSignature []byte `protobuf:"bytes,3,opt,name=accountSignature,proto3" json:"accountSignature,omitempty"`
DeviceSignature []byte `protobuf:"bytes,4,opt,name=deviceSignature,proto3" json:"deviceSignature,omitempty"`
}
func (x *ADVSignedDeviceIdentity) Reset() {
*x = ADVSignedDeviceIdentity{}
if protoimpl.UnsafeEnabled {
mi := &file_waAdv_WAAdv_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ADVSignedDeviceIdentity) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ADVSignedDeviceIdentity) ProtoMessage() {}
func (x *ADVSignedDeviceIdentity) ProtoReflect() protoreflect.Message {
mi := &file_waAdv_WAAdv_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ADVSignedDeviceIdentity.ProtoReflect.Descriptor instead.
func (*ADVSignedDeviceIdentity) Descriptor() ([]byte, []int) {
return file_waAdv_WAAdv_proto_rawDescGZIP(), []int{3}
}
func (x *ADVSignedDeviceIdentity) GetDetails() []byte {
if x != nil {
return x.Details
}
return nil
}
func (x *ADVSignedDeviceIdentity) GetAccountSignatureKey() []byte {
if x != nil {
return x.AccountSignatureKey
}
return nil
}
func (x *ADVSignedDeviceIdentity) GetAccountSignature() []byte {
if x != nil {
return x.AccountSignature
}
return nil
}
func (x *ADVSignedDeviceIdentity) GetDeviceSignature() []byte {
if x != nil {
return x.DeviceSignature
}
return nil
}
type ADVSignedDeviceIdentityHMAC struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Details []byte `protobuf:"bytes,1,opt,name=details,proto3" json:"details,omitempty"`
HMAC []byte `protobuf:"bytes,2,opt,name=HMAC,proto3" json:"HMAC,omitempty"`
AccountType ADVEncryptionType `protobuf:"varint,3,opt,name=accountType,proto3,enum=WAAdv.ADVEncryptionType" json:"accountType,omitempty"`
}
func (x *ADVSignedDeviceIdentityHMAC) Reset() {
*x = ADVSignedDeviceIdentityHMAC{}
if protoimpl.UnsafeEnabled {
mi := &file_waAdv_WAAdv_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ADVSignedDeviceIdentityHMAC) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ADVSignedDeviceIdentityHMAC) ProtoMessage() {}
func (x *ADVSignedDeviceIdentityHMAC) ProtoReflect() protoreflect.Message {
mi := &file_waAdv_WAAdv_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ADVSignedDeviceIdentityHMAC.ProtoReflect.Descriptor instead.
func (*ADVSignedDeviceIdentityHMAC) Descriptor() ([]byte, []int) {
return file_waAdv_WAAdv_proto_rawDescGZIP(), []int{4}
}
func (x *ADVSignedDeviceIdentityHMAC) GetDetails() []byte {
if x != nil {
return x.Details
}
return nil
}
func (x *ADVSignedDeviceIdentityHMAC) GetHMAC() []byte {
if x != nil {
return x.HMAC
}
return nil
}
func (x *ADVSignedDeviceIdentityHMAC) GetAccountType() ADVEncryptionType {
if x != nil {
return x.AccountType
}
return ADVEncryptionType_E2EE
}
var File_waAdv_WAAdv_proto protoreflect.FileDescriptor
//go:embed WAAdv.pb.raw
var file_waAdv_WAAdv_proto_rawDesc []byte
var (
file_waAdv_WAAdv_proto_rawDescOnce sync.Once
file_waAdv_WAAdv_proto_rawDescData = file_waAdv_WAAdv_proto_rawDesc
)
func file_waAdv_WAAdv_proto_rawDescGZIP() []byte {
file_waAdv_WAAdv_proto_rawDescOnce.Do(func() {
file_waAdv_WAAdv_proto_rawDescData = protoimpl.X.CompressGZIP(file_waAdv_WAAdv_proto_rawDescData)
})
return file_waAdv_WAAdv_proto_rawDescData
}
var file_waAdv_WAAdv_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_waAdv_WAAdv_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_waAdv_WAAdv_proto_goTypes = []interface{}{
(ADVEncryptionType)(0), // 0: WAAdv.ADVEncryptionType
(*ADVKeyIndexList)(nil), // 1: WAAdv.ADVKeyIndexList
(*ADVSignedKeyIndexList)(nil), // 2: WAAdv.ADVSignedKeyIndexList
(*ADVDeviceIdentity)(nil), // 3: WAAdv.ADVDeviceIdentity
(*ADVSignedDeviceIdentity)(nil), // 4: WAAdv.ADVSignedDeviceIdentity
(*ADVSignedDeviceIdentityHMAC)(nil), // 5: WAAdv.ADVSignedDeviceIdentityHMAC
}
var file_waAdv_WAAdv_proto_depIdxs = []int32{
0, // 0: WAAdv.ADVKeyIndexList.accountType:type_name -> WAAdv.ADVEncryptionType
0, // 1: WAAdv.ADVDeviceIdentity.accountType:type_name -> WAAdv.ADVEncryptionType
0, // 2: WAAdv.ADVDeviceIdentity.deviceType:type_name -> WAAdv.ADVEncryptionType
0, // 3: WAAdv.ADVSignedDeviceIdentityHMAC.accountType:type_name -> WAAdv.ADVEncryptionType
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_waAdv_WAAdv_proto_init() }
func file_waAdv_WAAdv_proto_init() {
if File_waAdv_WAAdv_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_waAdv_WAAdv_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ADVKeyIndexList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waAdv_WAAdv_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ADVSignedKeyIndexList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waAdv_WAAdv_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ADVDeviceIdentity); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waAdv_WAAdv_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ADVSignedDeviceIdentity); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waAdv_WAAdv_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ADVSignedDeviceIdentityHMAC); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_waAdv_WAAdv_proto_rawDesc,
NumEnums: 1,
NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_waAdv_WAAdv_proto_goTypes,
DependencyIndexes: file_waAdv_WAAdv_proto_depIdxs,
EnumInfos: file_waAdv_WAAdv_proto_enumTypes,
MessageInfos: file_waAdv_WAAdv_proto_msgTypes,
}.Build()
File_waAdv_WAAdv_proto = out.File
file_waAdv_WAAdv_proto_rawDesc = nil
file_waAdv_WAAdv_proto_goTypes = nil
file_waAdv_WAAdv_proto_depIdxs = nil
}

Binary file not shown.

View File

@ -0,0 +1,43 @@
syntax = "proto3";
package WAAdv;
option go_package = "go.mau.fi/whatsmeow/binary/armadillo/waAdv";
enum ADVEncryptionType {
E2EE = 0;
HOSTED = 1;
}
message ADVKeyIndexList {
uint32 rawID = 1;
uint64 timestamp = 2;
uint32 currentIndex = 3;
repeated uint32 validIndexes = 4 [packed=true];
ADVEncryptionType accountType = 5;
}
message ADVSignedKeyIndexList {
bytes details = 1;
bytes accountSignature = 2;
bytes accountSignatureKey = 3;
}
message ADVDeviceIdentity {
uint32 rawID = 1;
uint64 timestamp = 2;
uint32 keyIndex = 3;
ADVEncryptionType accountType = 4;
ADVEncryptionType deviceType = 5;
}
message ADVSignedDeviceIdentity {
bytes details = 1;
bytes accountSignatureKey = 2;
bytes accountSignature = 3;
bytes deviceSignature = 4;
}
message ADVSignedDeviceIdentityHMAC {
bytes details = 1;
bytes HMAC = 2;
ADVEncryptionType accountType = 3;
}

View File

@ -0,0 +1,245 @@
syntax = "proto3";
package WAArmadilloApplication;
option go_package = "go.mau.fi/whatsmeow/binary/armadillo/waArmadilloApplication";
import "waArmadilloXMA/WAArmadilloXMA.proto";
import "waCommon/WACommon.proto";
message Armadillo {
message Metadata {
}
message Payload {
oneof payload {
Content content = 1;
ApplicationData applicationData = 2;
Signal signal = 3;
SubProtocolPayload subProtocol = 4;
}
}
message SubProtocolPayload {
WACommon.FutureProofBehavior futureProof = 1;
}
message Signal {
message EncryptedBackupsSecrets {
message Epoch {
enum EpochStatus {
EPOCHSTATUS_UNKNOWN = 0;
ES_OPEN = 1;
ES_CLOSE = 2;
}
uint64 ID = 1;
bytes anonID = 2;
bytes rootKey = 3;
EpochStatus status = 4;
}
uint64 backupID = 1;
uint64 serverDataID = 2;
repeated Epoch epoch = 3;
bytes tempOcmfClientState = 4;
bytes mailboxRootKey = 5;
bytes obliviousValidationToken = 6;
}
oneof signal {
EncryptedBackupsSecrets encryptedBackupsSecrets = 1;
}
}
message ApplicationData {
message AIBotResponseMessage {
string summonToken = 1;
string messageText = 2;
string serializedExtras = 3;
}
message MetadataSyncAction {
message SyncMessageAction {
message ActionMessageDelete {
}
oneof action {
ActionMessageDelete messageDelete = 101;
}
WACommon.MessageKey key = 1;
}
message SyncChatAction {
message ActionChatRead {
SyncActionMessageRange messageRange = 1;
bool read = 2;
}
message ActionChatDelete {
SyncActionMessageRange messageRange = 1;
}
message ActionChatArchive {
SyncActionMessageRange messageRange = 1;
bool archived = 2;
}
oneof action {
ActionChatArchive chatArchive = 101;
ActionChatDelete chatDelete = 102;
ActionChatRead chatRead = 103;
}
string chatID = 1;
}
message SyncActionMessage {
WACommon.MessageKey key = 1;
int64 timestamp = 2;
}
message SyncActionMessageRange {
int64 lastMessageTimestamp = 1;
int64 lastSystemMessageTimestamp = 2;
repeated SyncActionMessage messages = 3;
}
oneof actionType {
SyncChatAction chatAction = 101;
SyncMessageAction messageAction = 102;
}
int64 actionTimestamp = 1;
}
message MetadataSyncNotification {
repeated MetadataSyncAction actions = 2;
}
oneof applicationData {
MetadataSyncNotification metadataSync = 1;
AIBotResponseMessage aiBotResponse = 2;
}
}
message Content {
message PaymentsTransactionMessage {
enum PaymentStatus {
PAYMENT_UNKNOWN = 0;
REQUEST_INITED = 4;
REQUEST_DECLINED = 5;
REQUEST_TRANSFER_INITED = 6;
REQUEST_TRANSFER_COMPLETED = 7;
REQUEST_TRANSFER_FAILED = 8;
REQUEST_CANCELED = 9;
REQUEST_EXPIRED = 10;
TRANSFER_INITED = 11;
TRANSFER_PENDING = 12;
TRANSFER_PENDING_RECIPIENT_VERIFICATION = 13;
TRANSFER_CANCELED = 14;
TRANSFER_COMPLETED = 15;
TRANSFER_NO_RECEIVER_CREDENTIAL_NO_RTS_PENDING_CANCELED = 16;
TRANSFER_NO_RECEIVER_CREDENTIAL_NO_RTS_PENDING_OTHER = 17;
TRANSFER_REFUNDED = 18;
TRANSFER_PARTIAL_REFUND = 19;
TRANSFER_CHARGED_BACK = 20;
TRANSFER_EXPIRED = 21;
TRANSFER_DECLINED = 22;
TRANSFER_UNAVAILABLE = 23;
}
uint64 transactionID = 1;
string amount = 2;
string currency = 3;
PaymentStatus paymentStatus = 4;
WAArmadilloXMA.ExtendedContentMessage extendedContentMessage = 5;
}
message NoteReplyMessage {
string noteID = 1;
WACommon.MessageText noteText = 2;
int64 noteTimestampMS = 3;
WACommon.MessageText noteReplyText = 4;
}
message BumpExistingMessage {
WACommon.MessageKey key = 1;
}
message ImageGalleryMessage {
repeated WACommon.SubProtocol images = 1;
}
message ScreenshotAction {
enum ScreenshotType {
SCREENSHOTTYPE_UNKNOWN = 0;
SCREENSHOT_IMAGE = 1;
SCREEN_RECORDING = 2;
}
ScreenshotType screenshotType = 1;
}
message ExtendedContentMessageWithSear {
string searID = 1;
bytes payload = 2;
string nativeURL = 3;
WACommon.SubProtocol searAssociatedMessage = 4;
string searSentWithMessageID = 5;
}
message RavenActionNotifMessage {
enum ActionType {
PLAYED = 0;
SCREENSHOT = 1;
FORCE_DISABLE = 2;
}
WACommon.MessageKey key = 1;
int64 actionTimestamp = 2;
ActionType actionType = 3;
}
message RavenMessage {
enum EphemeralType {
VIEW_ONCE = 0;
ALLOW_REPLAY = 1;
KEEP_IN_CHAT = 2;
}
oneof mediaContent {
WACommon.SubProtocol imageMessage = 2;
WACommon.SubProtocol videoMessage = 3;
}
EphemeralType ephemeralType = 1;
}
message CommonSticker {
enum StickerType {
STICKERTYPE_UNKNOWN = 0;
SMALL_LIKE = 1;
MEDIUM_LIKE = 2;
LARGE_LIKE = 3;
}
StickerType stickerType = 1;
}
oneof content {
CommonSticker commonSticker = 1;
ScreenshotAction screenshotAction = 3;
WAArmadilloXMA.ExtendedContentMessage extendedContentMessage = 4;
RavenMessage ravenMessage = 5;
RavenActionNotifMessage ravenActionNotifMessage = 6;
ExtendedContentMessageWithSear extendedMessageContentWithSear = 7;
ImageGalleryMessage imageGalleryMessage = 8;
PaymentsTransactionMessage paymentsTransactionMessage = 10;
BumpExistingMessage bumpExistingMessage = 11;
NoteReplyMessage noteReplyMessage = 13;
}
}
Payload payload = 1;
Metadata metadata = 2;
}

View File

@ -0,0 +1,3 @@
package waArmadilloApplication
func (*Armadillo) IsMessageApplicationSub() {}

View File

@ -0,0 +1,317 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc v3.21.12
// source: waArmadilloBackupMessage/WAArmadilloBackupMessage.proto
package waArmadilloBackupMessage
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
_ "embed"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type BackupMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Metadata *BackupMessage_Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
Payload []byte `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"`
}
func (x *BackupMessage) Reset() {
*x = BackupMessage{}
if protoimpl.UnsafeEnabled {
mi := &file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BackupMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BackupMessage) ProtoMessage() {}
func (x *BackupMessage) ProtoReflect() protoreflect.Message {
mi := &file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BackupMessage.ProtoReflect.Descriptor instead.
func (*BackupMessage) Descriptor() ([]byte, []int) {
return file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDescGZIP(), []int{0}
}
func (x *BackupMessage) GetMetadata() *BackupMessage_Metadata {
if x != nil {
return x.Metadata
}
return nil
}
func (x *BackupMessage) GetPayload() []byte {
if x != nil {
return x.Payload
}
return nil
}
type BackupMessage_Metadata struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
SenderID string `protobuf:"bytes,1,opt,name=senderID,proto3" json:"senderID,omitempty"`
MessageID string `protobuf:"bytes,2,opt,name=messageID,proto3" json:"messageID,omitempty"`
TimestampMS int64 `protobuf:"varint,3,opt,name=timestampMS,proto3" json:"timestampMS,omitempty"`
FrankingMetadata *BackupMessage_Metadata_FrankingMetadata `protobuf:"bytes,4,opt,name=frankingMetadata,proto3" json:"frankingMetadata,omitempty"`
PayloadVersion int32 `protobuf:"varint,5,opt,name=payloadVersion,proto3" json:"payloadVersion,omitempty"`
FutureProofBehavior int32 `protobuf:"varint,6,opt,name=futureProofBehavior,proto3" json:"futureProofBehavior,omitempty"`
}
func (x *BackupMessage_Metadata) Reset() {
*x = BackupMessage_Metadata{}
if protoimpl.UnsafeEnabled {
mi := &file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BackupMessage_Metadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BackupMessage_Metadata) ProtoMessage() {}
func (x *BackupMessage_Metadata) ProtoReflect() protoreflect.Message {
mi := &file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BackupMessage_Metadata.ProtoReflect.Descriptor instead.
func (*BackupMessage_Metadata) Descriptor() ([]byte, []int) {
return file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDescGZIP(), []int{0, 0}
}
func (x *BackupMessage_Metadata) GetSenderID() string {
if x != nil {
return x.SenderID
}
return ""
}
func (x *BackupMessage_Metadata) GetMessageID() string {
if x != nil {
return x.MessageID
}
return ""
}
func (x *BackupMessage_Metadata) GetTimestampMS() int64 {
if x != nil {
return x.TimestampMS
}
return 0
}
func (x *BackupMessage_Metadata) GetFrankingMetadata() *BackupMessage_Metadata_FrankingMetadata {
if x != nil {
return x.FrankingMetadata
}
return nil
}
func (x *BackupMessage_Metadata) GetPayloadVersion() int32 {
if x != nil {
return x.PayloadVersion
}
return 0
}
func (x *BackupMessage_Metadata) GetFutureProofBehavior() int32 {
if x != nil {
return x.FutureProofBehavior
}
return 0
}
type BackupMessage_Metadata_FrankingMetadata struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
FrankingTag []byte `protobuf:"bytes,3,opt,name=frankingTag,proto3" json:"frankingTag,omitempty"`
ReportingTag []byte `protobuf:"bytes,4,opt,name=reportingTag,proto3" json:"reportingTag,omitempty"`
}
func (x *BackupMessage_Metadata_FrankingMetadata) Reset() {
*x = BackupMessage_Metadata_FrankingMetadata{}
if protoimpl.UnsafeEnabled {
mi := &file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *BackupMessage_Metadata_FrankingMetadata) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BackupMessage_Metadata_FrankingMetadata) ProtoMessage() {}
func (x *BackupMessage_Metadata_FrankingMetadata) ProtoReflect() protoreflect.Message {
mi := &file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use BackupMessage_Metadata_FrankingMetadata.ProtoReflect.Descriptor instead.
func (*BackupMessage_Metadata_FrankingMetadata) Descriptor() ([]byte, []int) {
return file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDescGZIP(), []int{0, 0, 0}
}
func (x *BackupMessage_Metadata_FrankingMetadata) GetFrankingTag() []byte {
if x != nil {
return x.FrankingTag
}
return nil
}
func (x *BackupMessage_Metadata_FrankingMetadata) GetReportingTag() []byte {
if x != nil {
return x.ReportingTag
}
return nil
}
var File_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto protoreflect.FileDescriptor
//go:embed WAArmadilloBackupMessage.pb.raw
var file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDesc []byte
var (
file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDescOnce sync.Once
file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDescData = file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDesc
)
func file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDescGZIP() []byte {
file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDescOnce.Do(func() {
file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDescData = protoimpl.X.CompressGZIP(file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDescData)
})
return file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDescData
}
var file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_goTypes = []interface{}{
(*BackupMessage)(nil), // 0: WAArmadilloBackupMessage.BackupMessage
(*BackupMessage_Metadata)(nil), // 1: WAArmadilloBackupMessage.BackupMessage.Metadata
(*BackupMessage_Metadata_FrankingMetadata)(nil), // 2: WAArmadilloBackupMessage.BackupMessage.Metadata.FrankingMetadata
}
var file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_depIdxs = []int32{
1, // 0: WAArmadilloBackupMessage.BackupMessage.metadata:type_name -> WAArmadilloBackupMessage.BackupMessage.Metadata
2, // 1: WAArmadilloBackupMessage.BackupMessage.Metadata.frankingMetadata:type_name -> WAArmadilloBackupMessage.BackupMessage.Metadata.FrankingMetadata
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_init() }
func file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_init() {
if File_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*BackupMessage); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*BackupMessage_Metadata); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*BackupMessage_Metadata_FrankingMetadata); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDesc,
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_goTypes,
DependencyIndexes: file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_depIdxs,
MessageInfos: file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_msgTypes,
}.Build()
File_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto = out.File
file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_rawDesc = nil
file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_goTypes = nil
file_waArmadilloBackupMessage_WAArmadilloBackupMessage_proto_depIdxs = nil
}

View File

@ -0,0 +1,15 @@
7waArmadilloBackupMessage/WAArmadilloBackupMessage.protoWAArmadilloBackupMessage"ƒ
BackupMessageL
metadata ( 20.WAArmadilloBackupMessage.BackupMessage.MetadataRmetadata
payload ( Rpayload
Metadata
senderID ( RsenderID
messageID ( R messageID
timestampMS (R timestampMSm
frankingMetadata ( 2A.WAArmadilloBackupMessage.BackupMessage.Metadata.FrankingMetadataRfrankingMetadata&
payloadVersion (RpayloadVersion0
futureProofBehavior (RfutureProofBehaviorX
FrankingMetadata
frankingTag ( R frankingTag"
reportingTag ( R reportingTagB?Z=go.mau.fi/whatsmeow/binary/armadillo/waArmadilloBackupMessagebproto3

View File

@ -0,0 +1,22 @@
syntax = "proto3";
package WAArmadilloBackupMessage;
option go_package = "go.mau.fi/whatsmeow/binary/armadillo/waArmadilloBackupMessage";
message BackupMessage {
message Metadata {
message FrankingMetadata {
bytes frankingTag = 3;
bytes reportingTag = 4;
}
string senderID = 1;
string messageID = 2;
int64 timestampMS = 3;
FrankingMetadata frankingMetadata = 4;
int32 payloadVersion = 5;
int32 futureProofBehavior = 6;
}
Metadata metadata = 1;
bytes payload = 2;
}

View File

@ -0,0 +1,231 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc v3.21.12
// source: waArmadilloICDC/WAArmadilloICDC.proto
package waArmadilloICDC
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
_ "embed"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ICDCIdentityList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Seq int32 `protobuf:"varint,1,opt,name=seq,proto3" json:"seq,omitempty"`
Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Devices [][]byte `protobuf:"bytes,3,rep,name=devices,proto3" json:"devices,omitempty"`
SigningDeviceIndex int32 `protobuf:"varint,4,opt,name=signingDeviceIndex,proto3" json:"signingDeviceIndex,omitempty"`
}
func (x *ICDCIdentityList) Reset() {
*x = ICDCIdentityList{}
if protoimpl.UnsafeEnabled {
mi := &file_waArmadilloICDC_WAArmadilloICDC_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ICDCIdentityList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ICDCIdentityList) ProtoMessage() {}
func (x *ICDCIdentityList) ProtoReflect() protoreflect.Message {
mi := &file_waArmadilloICDC_WAArmadilloICDC_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ICDCIdentityList.ProtoReflect.Descriptor instead.
func (*ICDCIdentityList) Descriptor() ([]byte, []int) {
return file_waArmadilloICDC_WAArmadilloICDC_proto_rawDescGZIP(), []int{0}
}
func (x *ICDCIdentityList) GetSeq() int32 {
if x != nil {
return x.Seq
}
return 0
}
func (x *ICDCIdentityList) GetTimestamp() int64 {
if x != nil {
return x.Timestamp
}
return 0
}
func (x *ICDCIdentityList) GetDevices() [][]byte {
if x != nil {
return x.Devices
}
return nil
}
func (x *ICDCIdentityList) GetSigningDeviceIndex() int32 {
if x != nil {
return x.SigningDeviceIndex
}
return 0
}
type SignedICDCIdentityList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Details []byte `protobuf:"bytes,1,opt,name=details,proto3" json:"details,omitempty"`
Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
}
func (x *SignedICDCIdentityList) Reset() {
*x = SignedICDCIdentityList{}
if protoimpl.UnsafeEnabled {
mi := &file_waArmadilloICDC_WAArmadilloICDC_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SignedICDCIdentityList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SignedICDCIdentityList) ProtoMessage() {}
func (x *SignedICDCIdentityList) ProtoReflect() protoreflect.Message {
mi := &file_waArmadilloICDC_WAArmadilloICDC_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SignedICDCIdentityList.ProtoReflect.Descriptor instead.
func (*SignedICDCIdentityList) Descriptor() ([]byte, []int) {
return file_waArmadilloICDC_WAArmadilloICDC_proto_rawDescGZIP(), []int{1}
}
func (x *SignedICDCIdentityList) GetDetails() []byte {
if x != nil {
return x.Details
}
return nil
}
func (x *SignedICDCIdentityList) GetSignature() []byte {
if x != nil {
return x.Signature
}
return nil
}
var File_waArmadilloICDC_WAArmadilloICDC_proto protoreflect.FileDescriptor
//go:embed WAArmadilloICDC.pb.raw
var file_waArmadilloICDC_WAArmadilloICDC_proto_rawDesc []byte
var (
file_waArmadilloICDC_WAArmadilloICDC_proto_rawDescOnce sync.Once
file_waArmadilloICDC_WAArmadilloICDC_proto_rawDescData = file_waArmadilloICDC_WAArmadilloICDC_proto_rawDesc
)
func file_waArmadilloICDC_WAArmadilloICDC_proto_rawDescGZIP() []byte {
file_waArmadilloICDC_WAArmadilloICDC_proto_rawDescOnce.Do(func() {
file_waArmadilloICDC_WAArmadilloICDC_proto_rawDescData = protoimpl.X.CompressGZIP(file_waArmadilloICDC_WAArmadilloICDC_proto_rawDescData)
})
return file_waArmadilloICDC_WAArmadilloICDC_proto_rawDescData
}
var file_waArmadilloICDC_WAArmadilloICDC_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_waArmadilloICDC_WAArmadilloICDC_proto_goTypes = []interface{}{
(*ICDCIdentityList)(nil), // 0: WAArmadilloICDC.ICDCIdentityList
(*SignedICDCIdentityList)(nil), // 1: WAArmadilloICDC.SignedICDCIdentityList
}
var file_waArmadilloICDC_WAArmadilloICDC_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_waArmadilloICDC_WAArmadilloICDC_proto_init() }
func file_waArmadilloICDC_WAArmadilloICDC_proto_init() {
if File_waArmadilloICDC_WAArmadilloICDC_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_waArmadilloICDC_WAArmadilloICDC_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ICDCIdentityList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waArmadilloICDC_WAArmadilloICDC_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SignedICDCIdentityList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_waArmadilloICDC_WAArmadilloICDC_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_waArmadilloICDC_WAArmadilloICDC_proto_goTypes,
DependencyIndexes: file_waArmadilloICDC_WAArmadilloICDC_proto_depIdxs,
MessageInfos: file_waArmadilloICDC_WAArmadilloICDC_proto_msgTypes,
}.Build()
File_waArmadilloICDC_WAArmadilloICDC_proto = out.File
file_waArmadilloICDC_WAArmadilloICDC_proto_rawDesc = nil
file_waArmadilloICDC_WAArmadilloICDC_proto_goTypes = nil
file_waArmadilloICDC_WAArmadilloICDC_proto_depIdxs = nil
}

View File

@ -0,0 +1,10 @@
%waArmadilloICDC/WAArmadilloICDC.protoWAArmadilloICDC"Œ
ICDCIdentityList
seq (Rseq
timestamp (R timestamp
devices ( Rdevices.
signingDeviceIndex (RsigningDeviceIndex"P
SignedICDCIdentityList
details ( Rdetails
signature ( R signatureB6Z4go.mau.fi/whatsmeow/binary/armadillo/waArmadilloICDCbproto3

View File

@ -0,0 +1,15 @@
syntax = "proto3";
package WAArmadilloICDC;
option go_package = "go.mau.fi/whatsmeow/binary/armadillo/waArmadilloICDC";
message ICDCIdentityList {
int32 seq = 1;
int64 timestamp = 2;
repeated bytes devices = 3;
int32 signingDeviceIndex = 4;
}
message SignedICDCIdentityList {
bytes details = 1;
bytes signature = 2;
}

View File

@ -0,0 +1,785 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc v3.21.12
// source: waArmadilloXMA/WAArmadilloXMA.proto
package waArmadilloXMA
import (
reflect "reflect"
sync "sync"
waCommon "go.mau.fi/whatsmeow/binary/armadillo/waCommon"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
_ "embed"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ExtendedContentMessage_OverlayIconGlyph int32
const (
ExtendedContentMessage_INFO ExtendedContentMessage_OverlayIconGlyph = 0
ExtendedContentMessage_EYE_OFF ExtendedContentMessage_OverlayIconGlyph = 1
ExtendedContentMessage_NEWS_OFF ExtendedContentMessage_OverlayIconGlyph = 2
ExtendedContentMessage_WARNING ExtendedContentMessage_OverlayIconGlyph = 3
ExtendedContentMessage_PRIVATE ExtendedContentMessage_OverlayIconGlyph = 4
ExtendedContentMessage_NONE ExtendedContentMessage_OverlayIconGlyph = 5
ExtendedContentMessage_MEDIA_LABEL ExtendedContentMessage_OverlayIconGlyph = 6
ExtendedContentMessage_POST_COVER ExtendedContentMessage_OverlayIconGlyph = 7
ExtendedContentMessage_POST_LABEL ExtendedContentMessage_OverlayIconGlyph = 8
ExtendedContentMessage_WARNING_SCREENS ExtendedContentMessage_OverlayIconGlyph = 9
)
// Enum value maps for ExtendedContentMessage_OverlayIconGlyph.
var (
ExtendedContentMessage_OverlayIconGlyph_name = map[int32]string{
0: "INFO",
1: "EYE_OFF",
2: "NEWS_OFF",
3: "WARNING",
4: "PRIVATE",
5: "NONE",
6: "MEDIA_LABEL",
7: "POST_COVER",
8: "POST_LABEL",
9: "WARNING_SCREENS",
}
ExtendedContentMessage_OverlayIconGlyph_value = map[string]int32{
"INFO": 0,
"EYE_OFF": 1,
"NEWS_OFF": 2,
"WARNING": 3,
"PRIVATE": 4,
"NONE": 5,
"MEDIA_LABEL": 6,
"POST_COVER": 7,
"POST_LABEL": 8,
"WARNING_SCREENS": 9,
}
)
func (x ExtendedContentMessage_OverlayIconGlyph) Enum() *ExtendedContentMessage_OverlayIconGlyph {
p := new(ExtendedContentMessage_OverlayIconGlyph)
*p = x
return p
}
func (x ExtendedContentMessage_OverlayIconGlyph) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ExtendedContentMessage_OverlayIconGlyph) Descriptor() protoreflect.EnumDescriptor {
return file_waArmadilloXMA_WAArmadilloXMA_proto_enumTypes[0].Descriptor()
}
func (ExtendedContentMessage_OverlayIconGlyph) Type() protoreflect.EnumType {
return &file_waArmadilloXMA_WAArmadilloXMA_proto_enumTypes[0]
}
func (x ExtendedContentMessage_OverlayIconGlyph) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ExtendedContentMessage_OverlayIconGlyph.Descriptor instead.
func (ExtendedContentMessage_OverlayIconGlyph) EnumDescriptor() ([]byte, []int) {
return file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescGZIP(), []int{0, 0}
}
type ExtendedContentMessage_CtaButtonType int32
const (
ExtendedContentMessage_CTABUTTONTYPE_UNKNOWN ExtendedContentMessage_CtaButtonType = 0
ExtendedContentMessage_OPEN_NATIVE ExtendedContentMessage_CtaButtonType = 11
)
// Enum value maps for ExtendedContentMessage_CtaButtonType.
var (
ExtendedContentMessage_CtaButtonType_name = map[int32]string{
0: "CTABUTTONTYPE_UNKNOWN",
11: "OPEN_NATIVE",
}
ExtendedContentMessage_CtaButtonType_value = map[string]int32{
"CTABUTTONTYPE_UNKNOWN": 0,
"OPEN_NATIVE": 11,
}
)
func (x ExtendedContentMessage_CtaButtonType) Enum() *ExtendedContentMessage_CtaButtonType {
p := new(ExtendedContentMessage_CtaButtonType)
*p = x
return p
}
func (x ExtendedContentMessage_CtaButtonType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ExtendedContentMessage_CtaButtonType) Descriptor() protoreflect.EnumDescriptor {
return file_waArmadilloXMA_WAArmadilloXMA_proto_enumTypes[1].Descriptor()
}
func (ExtendedContentMessage_CtaButtonType) Type() protoreflect.EnumType {
return &file_waArmadilloXMA_WAArmadilloXMA_proto_enumTypes[1]
}
func (x ExtendedContentMessage_CtaButtonType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ExtendedContentMessage_CtaButtonType.Descriptor instead.
func (ExtendedContentMessage_CtaButtonType) EnumDescriptor() ([]byte, []int) {
return file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescGZIP(), []int{0, 1}
}
type ExtendedContentMessage_XmaLayoutType int32
const (
ExtendedContentMessage_SINGLE ExtendedContentMessage_XmaLayoutType = 0
ExtendedContentMessage_PORTRAIT ExtendedContentMessage_XmaLayoutType = 3
ExtendedContentMessage_STANDARD_DXMA ExtendedContentMessage_XmaLayoutType = 12
ExtendedContentMessage_LIST_DXMA ExtendedContentMessage_XmaLayoutType = 15
)
// Enum value maps for ExtendedContentMessage_XmaLayoutType.
var (
ExtendedContentMessage_XmaLayoutType_name = map[int32]string{
0: "SINGLE",
3: "PORTRAIT",
12: "STANDARD_DXMA",
15: "LIST_DXMA",
}
ExtendedContentMessage_XmaLayoutType_value = map[string]int32{
"SINGLE": 0,
"PORTRAIT": 3,
"STANDARD_DXMA": 12,
"LIST_DXMA": 15,
}
)
func (x ExtendedContentMessage_XmaLayoutType) Enum() *ExtendedContentMessage_XmaLayoutType {
p := new(ExtendedContentMessage_XmaLayoutType)
*p = x
return p
}
func (x ExtendedContentMessage_XmaLayoutType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ExtendedContentMessage_XmaLayoutType) Descriptor() protoreflect.EnumDescriptor {
return file_waArmadilloXMA_WAArmadilloXMA_proto_enumTypes[2].Descriptor()
}
func (ExtendedContentMessage_XmaLayoutType) Type() protoreflect.EnumType {
return &file_waArmadilloXMA_WAArmadilloXMA_proto_enumTypes[2]
}
func (x ExtendedContentMessage_XmaLayoutType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ExtendedContentMessage_XmaLayoutType.Descriptor instead.
func (ExtendedContentMessage_XmaLayoutType) EnumDescriptor() ([]byte, []int) {
return file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescGZIP(), []int{0, 2}
}
type ExtendedContentMessage_ExtendedContentType int32
const (
ExtendedContentMessage_EXTENDEDCONTENTTYPE_UNKNOWN ExtendedContentMessage_ExtendedContentType = 0
ExtendedContentMessage_IG_STORY_PHOTO_MENTION ExtendedContentMessage_ExtendedContentType = 4
ExtendedContentMessage_IG_SINGLE_IMAGE_POST_SHARE ExtendedContentMessage_ExtendedContentType = 9
ExtendedContentMessage_IG_MULTIPOST_SHARE ExtendedContentMessage_ExtendedContentType = 10
ExtendedContentMessage_IG_SINGLE_VIDEO_POST_SHARE ExtendedContentMessage_ExtendedContentType = 11
ExtendedContentMessage_IG_STORY_PHOTO_SHARE ExtendedContentMessage_ExtendedContentType = 12
ExtendedContentMessage_IG_STORY_VIDEO_SHARE ExtendedContentMessage_ExtendedContentType = 13
ExtendedContentMessage_IG_CLIPS_SHARE ExtendedContentMessage_ExtendedContentType = 14
ExtendedContentMessage_IG_IGTV_SHARE ExtendedContentMessage_ExtendedContentType = 15
ExtendedContentMessage_IG_SHOP_SHARE ExtendedContentMessage_ExtendedContentType = 16
ExtendedContentMessage_IG_PROFILE_SHARE ExtendedContentMessage_ExtendedContentType = 19
ExtendedContentMessage_IG_STORY_PHOTO_HIGHLIGHT_SHARE ExtendedContentMessage_ExtendedContentType = 20
ExtendedContentMessage_IG_STORY_VIDEO_HIGHLIGHT_SHARE ExtendedContentMessage_ExtendedContentType = 21
ExtendedContentMessage_IG_STORY_REPLY ExtendedContentMessage_ExtendedContentType = 22
ExtendedContentMessage_IG_STORY_REACTION ExtendedContentMessage_ExtendedContentType = 23
ExtendedContentMessage_IG_STORY_VIDEO_MENTION ExtendedContentMessage_ExtendedContentType = 24
ExtendedContentMessage_IG_STORY_HIGHLIGHT_REPLY ExtendedContentMessage_ExtendedContentType = 25
ExtendedContentMessage_IG_STORY_HIGHLIGHT_REACTION ExtendedContentMessage_ExtendedContentType = 26
ExtendedContentMessage_IG_EXTERNAL_LINK ExtendedContentMessage_ExtendedContentType = 27
ExtendedContentMessage_IG_RECEIVER_FETCH ExtendedContentMessage_ExtendedContentType = 28
ExtendedContentMessage_FB_FEED_SHARE ExtendedContentMessage_ExtendedContentType = 1000
ExtendedContentMessage_FB_STORY_REPLY ExtendedContentMessage_ExtendedContentType = 1001
ExtendedContentMessage_FB_STORY_SHARE ExtendedContentMessage_ExtendedContentType = 1002
ExtendedContentMessage_FB_STORY_MENTION ExtendedContentMessage_ExtendedContentType = 1003
ExtendedContentMessage_FB_FEED_VIDEO_SHARE ExtendedContentMessage_ExtendedContentType = 1004
ExtendedContentMessage_FB_GAMING_CUSTOM_UPDATE ExtendedContentMessage_ExtendedContentType = 1005
ExtendedContentMessage_FB_PRODUCER_STORY_REPLY ExtendedContentMessage_ExtendedContentType = 1006
ExtendedContentMessage_FB_EVENT ExtendedContentMessage_ExtendedContentType = 1007
ExtendedContentMessage_FB_FEED_POST_PRIVATE_REPLY ExtendedContentMessage_ExtendedContentType = 1008
ExtendedContentMessage_FB_SHORT ExtendedContentMessage_ExtendedContentType = 1009
ExtendedContentMessage_FB_COMMENT_MENTION_SHARE ExtendedContentMessage_ExtendedContentType = 1010
ExtendedContentMessage_MSG_EXTERNAL_LINK_SHARE ExtendedContentMessage_ExtendedContentType = 2000
ExtendedContentMessage_MSG_P2P_PAYMENT ExtendedContentMessage_ExtendedContentType = 2001
ExtendedContentMessage_MSG_LOCATION_SHARING ExtendedContentMessage_ExtendedContentType = 2002
ExtendedContentMessage_MSG_LOCATION_SHARING_V2 ExtendedContentMessage_ExtendedContentType = 2003
ExtendedContentMessage_MSG_HIGHLIGHTS_TAB_FRIEND_UPDATES_REPLY ExtendedContentMessage_ExtendedContentType = 2004
ExtendedContentMessage_MSG_HIGHLIGHTS_TAB_LOCAL_EVENT_REPLY ExtendedContentMessage_ExtendedContentType = 2005
ExtendedContentMessage_MSG_RECEIVER_FETCH ExtendedContentMessage_ExtendedContentType = 2006
ExtendedContentMessage_MSG_IG_MEDIA_SHARE ExtendedContentMessage_ExtendedContentType = 2007
ExtendedContentMessage_MSG_GEN_AI_SEARCH_PLUGIN_RESPONSE ExtendedContentMessage_ExtendedContentType = 2008
ExtendedContentMessage_MSG_REELS_LIST ExtendedContentMessage_ExtendedContentType = 2009
ExtendedContentMessage_MSG_CONTACT ExtendedContentMessage_ExtendedContentType = 2010
ExtendedContentMessage_RTC_AUDIO_CALL ExtendedContentMessage_ExtendedContentType = 3000
ExtendedContentMessage_RTC_VIDEO_CALL ExtendedContentMessage_ExtendedContentType = 3001
ExtendedContentMessage_RTC_MISSED_AUDIO_CALL ExtendedContentMessage_ExtendedContentType = 3002
ExtendedContentMessage_RTC_MISSED_VIDEO_CALL ExtendedContentMessage_ExtendedContentType = 3003
ExtendedContentMessage_RTC_GROUP_AUDIO_CALL ExtendedContentMessage_ExtendedContentType = 3004
ExtendedContentMessage_RTC_GROUP_VIDEO_CALL ExtendedContentMessage_ExtendedContentType = 3005
ExtendedContentMessage_RTC_MISSED_GROUP_AUDIO_CALL ExtendedContentMessage_ExtendedContentType = 3006
ExtendedContentMessage_RTC_MISSED_GROUP_VIDEO_CALL ExtendedContentMessage_ExtendedContentType = 3007
ExtendedContentMessage_DATACLASS_SENDER_COPY ExtendedContentMessage_ExtendedContentType = 4000
)
// Enum value maps for ExtendedContentMessage_ExtendedContentType.
var (
ExtendedContentMessage_ExtendedContentType_name = map[int32]string{
0: "EXTENDEDCONTENTTYPE_UNKNOWN",
4: "IG_STORY_PHOTO_MENTION",
9: "IG_SINGLE_IMAGE_POST_SHARE",
10: "IG_MULTIPOST_SHARE",
11: "IG_SINGLE_VIDEO_POST_SHARE",
12: "IG_STORY_PHOTO_SHARE",
13: "IG_STORY_VIDEO_SHARE",
14: "IG_CLIPS_SHARE",
15: "IG_IGTV_SHARE",
16: "IG_SHOP_SHARE",
19: "IG_PROFILE_SHARE",
20: "IG_STORY_PHOTO_HIGHLIGHT_SHARE",
21: "IG_STORY_VIDEO_HIGHLIGHT_SHARE",
22: "IG_STORY_REPLY",
23: "IG_STORY_REACTION",
24: "IG_STORY_VIDEO_MENTION",
25: "IG_STORY_HIGHLIGHT_REPLY",
26: "IG_STORY_HIGHLIGHT_REACTION",
27: "IG_EXTERNAL_LINK",
28: "IG_RECEIVER_FETCH",
1000: "FB_FEED_SHARE",
1001: "FB_STORY_REPLY",
1002: "FB_STORY_SHARE",
1003: "FB_STORY_MENTION",
1004: "FB_FEED_VIDEO_SHARE",
1005: "FB_GAMING_CUSTOM_UPDATE",
1006: "FB_PRODUCER_STORY_REPLY",
1007: "FB_EVENT",
1008: "FB_FEED_POST_PRIVATE_REPLY",
1009: "FB_SHORT",
1010: "FB_COMMENT_MENTION_SHARE",
2000: "MSG_EXTERNAL_LINK_SHARE",
2001: "MSG_P2P_PAYMENT",
2002: "MSG_LOCATION_SHARING",
2003: "MSG_LOCATION_SHARING_V2",
2004: "MSG_HIGHLIGHTS_TAB_FRIEND_UPDATES_REPLY",
2005: "MSG_HIGHLIGHTS_TAB_LOCAL_EVENT_REPLY",
2006: "MSG_RECEIVER_FETCH",
2007: "MSG_IG_MEDIA_SHARE",
2008: "MSG_GEN_AI_SEARCH_PLUGIN_RESPONSE",
2009: "MSG_REELS_LIST",
2010: "MSG_CONTACT",
3000: "RTC_AUDIO_CALL",
3001: "RTC_VIDEO_CALL",
3002: "RTC_MISSED_AUDIO_CALL",
3003: "RTC_MISSED_VIDEO_CALL",
3004: "RTC_GROUP_AUDIO_CALL",
3005: "RTC_GROUP_VIDEO_CALL",
3006: "RTC_MISSED_GROUP_AUDIO_CALL",
3007: "RTC_MISSED_GROUP_VIDEO_CALL",
4000: "DATACLASS_SENDER_COPY",
}
ExtendedContentMessage_ExtendedContentType_value = map[string]int32{
"EXTENDEDCONTENTTYPE_UNKNOWN": 0,
"IG_STORY_PHOTO_MENTION": 4,
"IG_SINGLE_IMAGE_POST_SHARE": 9,
"IG_MULTIPOST_SHARE": 10,
"IG_SINGLE_VIDEO_POST_SHARE": 11,
"IG_STORY_PHOTO_SHARE": 12,
"IG_STORY_VIDEO_SHARE": 13,
"IG_CLIPS_SHARE": 14,
"IG_IGTV_SHARE": 15,
"IG_SHOP_SHARE": 16,
"IG_PROFILE_SHARE": 19,
"IG_STORY_PHOTO_HIGHLIGHT_SHARE": 20,
"IG_STORY_VIDEO_HIGHLIGHT_SHARE": 21,
"IG_STORY_REPLY": 22,
"IG_STORY_REACTION": 23,
"IG_STORY_VIDEO_MENTION": 24,
"IG_STORY_HIGHLIGHT_REPLY": 25,
"IG_STORY_HIGHLIGHT_REACTION": 26,
"IG_EXTERNAL_LINK": 27,
"IG_RECEIVER_FETCH": 28,
"FB_FEED_SHARE": 1000,
"FB_STORY_REPLY": 1001,
"FB_STORY_SHARE": 1002,
"FB_STORY_MENTION": 1003,
"FB_FEED_VIDEO_SHARE": 1004,
"FB_GAMING_CUSTOM_UPDATE": 1005,
"FB_PRODUCER_STORY_REPLY": 1006,
"FB_EVENT": 1007,
"FB_FEED_POST_PRIVATE_REPLY": 1008,
"FB_SHORT": 1009,
"FB_COMMENT_MENTION_SHARE": 1010,
"MSG_EXTERNAL_LINK_SHARE": 2000,
"MSG_P2P_PAYMENT": 2001,
"MSG_LOCATION_SHARING": 2002,
"MSG_LOCATION_SHARING_V2": 2003,
"MSG_HIGHLIGHTS_TAB_FRIEND_UPDATES_REPLY": 2004,
"MSG_HIGHLIGHTS_TAB_LOCAL_EVENT_REPLY": 2005,
"MSG_RECEIVER_FETCH": 2006,
"MSG_IG_MEDIA_SHARE": 2007,
"MSG_GEN_AI_SEARCH_PLUGIN_RESPONSE": 2008,
"MSG_REELS_LIST": 2009,
"MSG_CONTACT": 2010,
"RTC_AUDIO_CALL": 3000,
"RTC_VIDEO_CALL": 3001,
"RTC_MISSED_AUDIO_CALL": 3002,
"RTC_MISSED_VIDEO_CALL": 3003,
"RTC_GROUP_AUDIO_CALL": 3004,
"RTC_GROUP_VIDEO_CALL": 3005,
"RTC_MISSED_GROUP_AUDIO_CALL": 3006,
"RTC_MISSED_GROUP_VIDEO_CALL": 3007,
"DATACLASS_SENDER_COPY": 4000,
}
)
func (x ExtendedContentMessage_ExtendedContentType) Enum() *ExtendedContentMessage_ExtendedContentType {
p := new(ExtendedContentMessage_ExtendedContentType)
*p = x
return p
}
func (x ExtendedContentMessage_ExtendedContentType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ExtendedContentMessage_ExtendedContentType) Descriptor() protoreflect.EnumDescriptor {
return file_waArmadilloXMA_WAArmadilloXMA_proto_enumTypes[3].Descriptor()
}
func (ExtendedContentMessage_ExtendedContentType) Type() protoreflect.EnumType {
return &file_waArmadilloXMA_WAArmadilloXMA_proto_enumTypes[3]
}
func (x ExtendedContentMessage_ExtendedContentType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ExtendedContentMessage_ExtendedContentType.Descriptor instead.
func (ExtendedContentMessage_ExtendedContentType) EnumDescriptor() ([]byte, []int) {
return file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescGZIP(), []int{0, 3}
}
type ExtendedContentMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
AssociatedMessage *waCommon.SubProtocol `protobuf:"bytes,1,opt,name=associatedMessage,proto3" json:"associatedMessage,omitempty"`
TargetType ExtendedContentMessage_ExtendedContentType `protobuf:"varint,2,opt,name=targetType,proto3,enum=WAArmadilloXMA.ExtendedContentMessage_ExtendedContentType" json:"targetType,omitempty"`
TargetUsername string `protobuf:"bytes,3,opt,name=targetUsername,proto3" json:"targetUsername,omitempty"`
TargetID string `protobuf:"bytes,4,opt,name=targetID,proto3" json:"targetID,omitempty"`
TargetExpiringAtSec int64 `protobuf:"varint,5,opt,name=targetExpiringAtSec,proto3" json:"targetExpiringAtSec,omitempty"`
XmaLayoutType ExtendedContentMessage_XmaLayoutType `protobuf:"varint,6,opt,name=xmaLayoutType,proto3,enum=WAArmadilloXMA.ExtendedContentMessage_XmaLayoutType" json:"xmaLayoutType,omitempty"`
Ctas []*ExtendedContentMessage_CTA `protobuf:"bytes,7,rep,name=ctas,proto3" json:"ctas,omitempty"`
Previews []*waCommon.SubProtocol `protobuf:"bytes,8,rep,name=previews,proto3" json:"previews,omitempty"`
TitleText string `protobuf:"bytes,9,opt,name=titleText,proto3" json:"titleText,omitempty"`
SubtitleText string `protobuf:"bytes,10,opt,name=subtitleText,proto3" json:"subtitleText,omitempty"`
MaxTitleNumOfLines uint32 `protobuf:"varint,11,opt,name=maxTitleNumOfLines,proto3" json:"maxTitleNumOfLines,omitempty"`
MaxSubtitleNumOfLines uint32 `protobuf:"varint,12,opt,name=maxSubtitleNumOfLines,proto3" json:"maxSubtitleNumOfLines,omitempty"`
Favicon *waCommon.SubProtocol `protobuf:"bytes,13,opt,name=favicon,proto3" json:"favicon,omitempty"`
HeaderImage *waCommon.SubProtocol `protobuf:"bytes,14,opt,name=headerImage,proto3" json:"headerImage,omitempty"`
HeaderTitle string `protobuf:"bytes,15,opt,name=headerTitle,proto3" json:"headerTitle,omitempty"`
OverlayIconGlyph ExtendedContentMessage_OverlayIconGlyph `protobuf:"varint,16,opt,name=overlayIconGlyph,proto3,enum=WAArmadilloXMA.ExtendedContentMessage_OverlayIconGlyph" json:"overlayIconGlyph,omitempty"`
OverlayTitle string `protobuf:"bytes,17,opt,name=overlayTitle,proto3" json:"overlayTitle,omitempty"`
OverlayDescription string `protobuf:"bytes,18,opt,name=overlayDescription,proto3" json:"overlayDescription,omitempty"`
SentWithMessageID string `protobuf:"bytes,19,opt,name=sentWithMessageID,proto3" json:"sentWithMessageID,omitempty"`
MessageText string `protobuf:"bytes,20,opt,name=messageText,proto3" json:"messageText,omitempty"`
HeaderSubtitle string `protobuf:"bytes,21,opt,name=headerSubtitle,proto3" json:"headerSubtitle,omitempty"`
XmaDataclass string `protobuf:"bytes,22,opt,name=xmaDataclass,proto3" json:"xmaDataclass,omitempty"`
ContentRef string `protobuf:"bytes,23,opt,name=contentRef,proto3" json:"contentRef,omitempty"`
}
func (x *ExtendedContentMessage) Reset() {
*x = ExtendedContentMessage{}
if protoimpl.UnsafeEnabled {
mi := &file_waArmadilloXMA_WAArmadilloXMA_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ExtendedContentMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExtendedContentMessage) ProtoMessage() {}
func (x *ExtendedContentMessage) ProtoReflect() protoreflect.Message {
mi := &file_waArmadilloXMA_WAArmadilloXMA_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ExtendedContentMessage.ProtoReflect.Descriptor instead.
func (*ExtendedContentMessage) Descriptor() ([]byte, []int) {
return file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescGZIP(), []int{0}
}
func (x *ExtendedContentMessage) GetAssociatedMessage() *waCommon.SubProtocol {
if x != nil {
return x.AssociatedMessage
}
return nil
}
func (x *ExtendedContentMessage) GetTargetType() ExtendedContentMessage_ExtendedContentType {
if x != nil {
return x.TargetType
}
return ExtendedContentMessage_EXTENDEDCONTENTTYPE_UNKNOWN
}
func (x *ExtendedContentMessage) GetTargetUsername() string {
if x != nil {
return x.TargetUsername
}
return ""
}
func (x *ExtendedContentMessage) GetTargetID() string {
if x != nil {
return x.TargetID
}
return ""
}
func (x *ExtendedContentMessage) GetTargetExpiringAtSec() int64 {
if x != nil {
return x.TargetExpiringAtSec
}
return 0
}
func (x *ExtendedContentMessage) GetXmaLayoutType() ExtendedContentMessage_XmaLayoutType {
if x != nil {
return x.XmaLayoutType
}
return ExtendedContentMessage_SINGLE
}
func (x *ExtendedContentMessage) GetCtas() []*ExtendedContentMessage_CTA {
if x != nil {
return x.Ctas
}
return nil
}
func (x *ExtendedContentMessage) GetPreviews() []*waCommon.SubProtocol {
if x != nil {
return x.Previews
}
return nil
}
func (x *ExtendedContentMessage) GetTitleText() string {
if x != nil {
return x.TitleText
}
return ""
}
func (x *ExtendedContentMessage) GetSubtitleText() string {
if x != nil {
return x.SubtitleText
}
return ""
}
func (x *ExtendedContentMessage) GetMaxTitleNumOfLines() uint32 {
if x != nil {
return x.MaxTitleNumOfLines
}
return 0
}
func (x *ExtendedContentMessage) GetMaxSubtitleNumOfLines() uint32 {
if x != nil {
return x.MaxSubtitleNumOfLines
}
return 0
}
func (x *ExtendedContentMessage) GetFavicon() *waCommon.SubProtocol {
if x != nil {
return x.Favicon
}
return nil
}
func (x *ExtendedContentMessage) GetHeaderImage() *waCommon.SubProtocol {
if x != nil {
return x.HeaderImage
}
return nil
}
func (x *ExtendedContentMessage) GetHeaderTitle() string {
if x != nil {
return x.HeaderTitle
}
return ""
}
func (x *ExtendedContentMessage) GetOverlayIconGlyph() ExtendedContentMessage_OverlayIconGlyph {
if x != nil {
return x.OverlayIconGlyph
}
return ExtendedContentMessage_INFO
}
func (x *ExtendedContentMessage) GetOverlayTitle() string {
if x != nil {
return x.OverlayTitle
}
return ""
}
func (x *ExtendedContentMessage) GetOverlayDescription() string {
if x != nil {
return x.OverlayDescription
}
return ""
}
func (x *ExtendedContentMessage) GetSentWithMessageID() string {
if x != nil {
return x.SentWithMessageID
}
return ""
}
func (x *ExtendedContentMessage) GetMessageText() string {
if x != nil {
return x.MessageText
}
return ""
}
func (x *ExtendedContentMessage) GetHeaderSubtitle() string {
if x != nil {
return x.HeaderSubtitle
}
return ""
}
func (x *ExtendedContentMessage) GetXmaDataclass() string {
if x != nil {
return x.XmaDataclass
}
return ""
}
func (x *ExtendedContentMessage) GetContentRef() string {
if x != nil {
return x.ContentRef
}
return ""
}
type ExtendedContentMessage_CTA struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ButtonType ExtendedContentMessage_CtaButtonType `protobuf:"varint,1,opt,name=buttonType,proto3,enum=WAArmadilloXMA.ExtendedContentMessage_CtaButtonType" json:"buttonType,omitempty"`
Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
ActionURL string `protobuf:"bytes,3,opt,name=actionURL,proto3" json:"actionURL,omitempty"`
NativeURL string `protobuf:"bytes,4,opt,name=nativeURL,proto3" json:"nativeURL,omitempty"`
CtaType string `protobuf:"bytes,5,opt,name=ctaType,proto3" json:"ctaType,omitempty"`
}
func (x *ExtendedContentMessage_CTA) Reset() {
*x = ExtendedContentMessage_CTA{}
if protoimpl.UnsafeEnabled {
mi := &file_waArmadilloXMA_WAArmadilloXMA_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ExtendedContentMessage_CTA) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ExtendedContentMessage_CTA) ProtoMessage() {}
func (x *ExtendedContentMessage_CTA) ProtoReflect() protoreflect.Message {
mi := &file_waArmadilloXMA_WAArmadilloXMA_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ExtendedContentMessage_CTA.ProtoReflect.Descriptor instead.
func (*ExtendedContentMessage_CTA) Descriptor() ([]byte, []int) {
return file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescGZIP(), []int{0, 0}
}
func (x *ExtendedContentMessage_CTA) GetButtonType() ExtendedContentMessage_CtaButtonType {
if x != nil {
return x.ButtonType
}
return ExtendedContentMessage_CTABUTTONTYPE_UNKNOWN
}
func (x *ExtendedContentMessage_CTA) GetTitle() string {
if x != nil {
return x.Title
}
return ""
}
func (x *ExtendedContentMessage_CTA) GetActionURL() string {
if x != nil {
return x.ActionURL
}
return ""
}
func (x *ExtendedContentMessage_CTA) GetNativeURL() string {
if x != nil {
return x.NativeURL
}
return ""
}
func (x *ExtendedContentMessage_CTA) GetCtaType() string {
if x != nil {
return x.CtaType
}
return ""
}
var File_waArmadilloXMA_WAArmadilloXMA_proto protoreflect.FileDescriptor
//go:embed WAArmadilloXMA.pb.raw
var file_waArmadilloXMA_WAArmadilloXMA_proto_rawDesc []byte
var (
file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescOnce sync.Once
file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescData = file_waArmadilloXMA_WAArmadilloXMA_proto_rawDesc
)
func file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescGZIP() []byte {
file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescOnce.Do(func() {
file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescData = protoimpl.X.CompressGZIP(file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescData)
})
return file_waArmadilloXMA_WAArmadilloXMA_proto_rawDescData
}
var file_waArmadilloXMA_WAArmadilloXMA_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
var file_waArmadilloXMA_WAArmadilloXMA_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_waArmadilloXMA_WAArmadilloXMA_proto_goTypes = []interface{}{
(ExtendedContentMessage_OverlayIconGlyph)(0), // 0: WAArmadilloXMA.ExtendedContentMessage.OverlayIconGlyph
(ExtendedContentMessage_CtaButtonType)(0), // 1: WAArmadilloXMA.ExtendedContentMessage.CtaButtonType
(ExtendedContentMessage_XmaLayoutType)(0), // 2: WAArmadilloXMA.ExtendedContentMessage.XmaLayoutType
(ExtendedContentMessage_ExtendedContentType)(0), // 3: WAArmadilloXMA.ExtendedContentMessage.ExtendedContentType
(*ExtendedContentMessage)(nil), // 4: WAArmadilloXMA.ExtendedContentMessage
(*ExtendedContentMessage_CTA)(nil), // 5: WAArmadilloXMA.ExtendedContentMessage.CTA
(*waCommon.SubProtocol)(nil), // 6: WACommon.SubProtocol
}
var file_waArmadilloXMA_WAArmadilloXMA_proto_depIdxs = []int32{
6, // 0: WAArmadilloXMA.ExtendedContentMessage.associatedMessage:type_name -> WACommon.SubProtocol
3, // 1: WAArmadilloXMA.ExtendedContentMessage.targetType:type_name -> WAArmadilloXMA.ExtendedContentMessage.ExtendedContentType
2, // 2: WAArmadilloXMA.ExtendedContentMessage.xmaLayoutType:type_name -> WAArmadilloXMA.ExtendedContentMessage.XmaLayoutType
5, // 3: WAArmadilloXMA.ExtendedContentMessage.ctas:type_name -> WAArmadilloXMA.ExtendedContentMessage.CTA
6, // 4: WAArmadilloXMA.ExtendedContentMessage.previews:type_name -> WACommon.SubProtocol
6, // 5: WAArmadilloXMA.ExtendedContentMessage.favicon:type_name -> WACommon.SubProtocol
6, // 6: WAArmadilloXMA.ExtendedContentMessage.headerImage:type_name -> WACommon.SubProtocol
0, // 7: WAArmadilloXMA.ExtendedContentMessage.overlayIconGlyph:type_name -> WAArmadilloXMA.ExtendedContentMessage.OverlayIconGlyph
1, // 8: WAArmadilloXMA.ExtendedContentMessage.CTA.buttonType:type_name -> WAArmadilloXMA.ExtendedContentMessage.CtaButtonType
9, // [9:9] is the sub-list for method output_type
9, // [9:9] is the sub-list for method input_type
9, // [9:9] is the sub-list for extension type_name
9, // [9:9] is the sub-list for extension extendee
0, // [0:9] is the sub-list for field type_name
}
func init() { file_waArmadilloXMA_WAArmadilloXMA_proto_init() }
func file_waArmadilloXMA_WAArmadilloXMA_proto_init() {
if File_waArmadilloXMA_WAArmadilloXMA_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_waArmadilloXMA_WAArmadilloXMA_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ExtendedContentMessage); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waArmadilloXMA_WAArmadilloXMA_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ExtendedContentMessage_CTA); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_waArmadilloXMA_WAArmadilloXMA_proto_rawDesc,
NumEnums: 4,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_waArmadilloXMA_WAArmadilloXMA_proto_goTypes,
DependencyIndexes: file_waArmadilloXMA_WAArmadilloXMA_proto_depIdxs,
EnumInfos: file_waArmadilloXMA_WAArmadilloXMA_proto_enumTypes,
MessageInfos: file_waArmadilloXMA_WAArmadilloXMA_proto_msgTypes,
}.Build()
File_waArmadilloXMA_WAArmadilloXMA_proto = out.File
file_waArmadilloXMA_WAArmadilloXMA_proto_rawDesc = nil
file_waArmadilloXMA_WAArmadilloXMA_proto_goTypes = nil
file_waArmadilloXMA_WAArmadilloXMA_proto_depIdxs = nil
}

View File

@ -0,0 +1,118 @@
syntax = "proto3";
package WAArmadilloXMA;
option go_package = "go.mau.fi/whatsmeow/binary/armadillo/waArmadilloXMA";
import "waCommon/WACommon.proto";
message ExtendedContentMessage {
enum OverlayIconGlyph {
INFO = 0;
EYE_OFF = 1;
NEWS_OFF = 2;
WARNING = 3;
PRIVATE = 4;
NONE = 5;
MEDIA_LABEL = 6;
POST_COVER = 7;
POST_LABEL = 8;
WARNING_SCREENS = 9;
}
enum CtaButtonType {
CTABUTTONTYPE_UNKNOWN = 0;
OPEN_NATIVE = 11;
}
enum XmaLayoutType {
SINGLE = 0;
PORTRAIT = 3;
STANDARD_DXMA = 12;
LIST_DXMA = 15;
}
enum ExtendedContentType {
EXTENDEDCONTENTTYPE_UNKNOWN = 0;
IG_STORY_PHOTO_MENTION = 4;
IG_SINGLE_IMAGE_POST_SHARE = 9;
IG_MULTIPOST_SHARE = 10;
IG_SINGLE_VIDEO_POST_SHARE = 11;
IG_STORY_PHOTO_SHARE = 12;
IG_STORY_VIDEO_SHARE = 13;
IG_CLIPS_SHARE = 14;
IG_IGTV_SHARE = 15;
IG_SHOP_SHARE = 16;
IG_PROFILE_SHARE = 19;
IG_STORY_PHOTO_HIGHLIGHT_SHARE = 20;
IG_STORY_VIDEO_HIGHLIGHT_SHARE = 21;
IG_STORY_REPLY = 22;
IG_STORY_REACTION = 23;
IG_STORY_VIDEO_MENTION = 24;
IG_STORY_HIGHLIGHT_REPLY = 25;
IG_STORY_HIGHLIGHT_REACTION = 26;
IG_EXTERNAL_LINK = 27;
IG_RECEIVER_FETCH = 28;
FB_FEED_SHARE = 1000;
FB_STORY_REPLY = 1001;
FB_STORY_SHARE = 1002;
FB_STORY_MENTION = 1003;
FB_FEED_VIDEO_SHARE = 1004;
FB_GAMING_CUSTOM_UPDATE = 1005;
FB_PRODUCER_STORY_REPLY = 1006;
FB_EVENT = 1007;
FB_FEED_POST_PRIVATE_REPLY = 1008;
FB_SHORT = 1009;
FB_COMMENT_MENTION_SHARE = 1010;
MSG_EXTERNAL_LINK_SHARE = 2000;
MSG_P2P_PAYMENT = 2001;
MSG_LOCATION_SHARING = 2002;
MSG_LOCATION_SHARING_V2 = 2003;
MSG_HIGHLIGHTS_TAB_FRIEND_UPDATES_REPLY = 2004;
MSG_HIGHLIGHTS_TAB_LOCAL_EVENT_REPLY = 2005;
MSG_RECEIVER_FETCH = 2006;
MSG_IG_MEDIA_SHARE = 2007;
MSG_GEN_AI_SEARCH_PLUGIN_RESPONSE = 2008;
MSG_REELS_LIST = 2009;
MSG_CONTACT = 2010;
RTC_AUDIO_CALL = 3000;
RTC_VIDEO_CALL = 3001;
RTC_MISSED_AUDIO_CALL = 3002;
RTC_MISSED_VIDEO_CALL = 3003;
RTC_GROUP_AUDIO_CALL = 3004;
RTC_GROUP_VIDEO_CALL = 3005;
RTC_MISSED_GROUP_AUDIO_CALL = 3006;
RTC_MISSED_GROUP_VIDEO_CALL = 3007;
DATACLASS_SENDER_COPY = 4000;
}
message CTA {
CtaButtonType buttonType = 1;
string title = 2;
string actionURL = 3;
string nativeURL = 4;
string ctaType = 5;
}
WACommon.SubProtocol associatedMessage = 1;
ExtendedContentType targetType = 2;
string targetUsername = 3;
string targetID = 4;
int64 targetExpiringAtSec = 5;
XmaLayoutType xmaLayoutType = 6;
repeated CTA ctas = 7;
repeated WACommon.SubProtocol previews = 8;
string titleText = 9;
string subtitleText = 10;
uint32 maxTitleNumOfLines = 11;
uint32 maxSubtitleNumOfLines = 12;
WACommon.SubProtocol favicon = 13;
WACommon.SubProtocol headerImage = 14;
string headerTitle = 15;
OverlayIconGlyph overlayIconGlyph = 16;
string overlayTitle = 17;
string overlayDescription = 18;
string sentWithMessageID = 19;
string messageText = 20;
string headerSubtitle = 21;
string xmaDataclass = 22;
string contentRef = 23;
}

View File

@ -0,0 +1,469 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc v3.21.12
// source: waCert/WACert.proto
package waCert
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
_ "embed"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type NoiseCertificate struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Details []byte `protobuf:"bytes,1,opt,name=details,proto3" json:"details,omitempty"`
Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
}
func (x *NoiseCertificate) Reset() {
*x = NoiseCertificate{}
if protoimpl.UnsafeEnabled {
mi := &file_waCert_WACert_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NoiseCertificate) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NoiseCertificate) ProtoMessage() {}
func (x *NoiseCertificate) ProtoReflect() protoreflect.Message {
mi := &file_waCert_WACert_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NoiseCertificate.ProtoReflect.Descriptor instead.
func (*NoiseCertificate) Descriptor() ([]byte, []int) {
return file_waCert_WACert_proto_rawDescGZIP(), []int{0}
}
func (x *NoiseCertificate) GetDetails() []byte {
if x != nil {
return x.Details
}
return nil
}
func (x *NoiseCertificate) GetSignature() []byte {
if x != nil {
return x.Signature
}
return nil
}
type CertChain struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Leaf *CertChain_NoiseCertificate `protobuf:"bytes,1,opt,name=leaf,proto3" json:"leaf,omitempty"`
Intermediate *CertChain_NoiseCertificate `protobuf:"bytes,2,opt,name=intermediate,proto3" json:"intermediate,omitempty"`
}
func (x *CertChain) Reset() {
*x = CertChain{}
if protoimpl.UnsafeEnabled {
mi := &file_waCert_WACert_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CertChain) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CertChain) ProtoMessage() {}
func (x *CertChain) ProtoReflect() protoreflect.Message {
mi := &file_waCert_WACert_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CertChain.ProtoReflect.Descriptor instead.
func (*CertChain) Descriptor() ([]byte, []int) {
return file_waCert_WACert_proto_rawDescGZIP(), []int{1}
}
func (x *CertChain) GetLeaf() *CertChain_NoiseCertificate {
if x != nil {
return x.Leaf
}
return nil
}
func (x *CertChain) GetIntermediate() *CertChain_NoiseCertificate {
if x != nil {
return x.Intermediate
}
return nil
}
type NoiseCertificate_Details struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Serial uint32 `protobuf:"varint,1,opt,name=serial,proto3" json:"serial,omitempty"`
Issuer string `protobuf:"bytes,2,opt,name=issuer,proto3" json:"issuer,omitempty"`
Expires uint64 `protobuf:"varint,3,opt,name=expires,proto3" json:"expires,omitempty"`
Subject string `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"`
Key []byte `protobuf:"bytes,5,opt,name=key,proto3" json:"key,omitempty"`
}
func (x *NoiseCertificate_Details) Reset() {
*x = NoiseCertificate_Details{}
if protoimpl.UnsafeEnabled {
mi := &file_waCert_WACert_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *NoiseCertificate_Details) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*NoiseCertificate_Details) ProtoMessage() {}
func (x *NoiseCertificate_Details) ProtoReflect() protoreflect.Message {
mi := &file_waCert_WACert_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use NoiseCertificate_Details.ProtoReflect.Descriptor instead.
func (*NoiseCertificate_Details) Descriptor() ([]byte, []int) {
return file_waCert_WACert_proto_rawDescGZIP(), []int{0, 0}
}
func (x *NoiseCertificate_Details) GetSerial() uint32 {
if x != nil {
return x.Serial
}
return 0
}
func (x *NoiseCertificate_Details) GetIssuer() string {
if x != nil {
return x.Issuer
}
return ""
}
func (x *NoiseCertificate_Details) GetExpires() uint64 {
if x != nil {
return x.Expires
}
return 0
}
func (x *NoiseCertificate_Details) GetSubject() string {
if x != nil {
return x.Subject
}
return ""
}
func (x *NoiseCertificate_Details) GetKey() []byte {
if x != nil {
return x.Key
}
return nil
}
type CertChain_NoiseCertificate struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Details []byte `protobuf:"bytes,1,opt,name=details,proto3" json:"details,omitempty"`
Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
}
func (x *CertChain_NoiseCertificate) Reset() {
*x = CertChain_NoiseCertificate{}
if protoimpl.UnsafeEnabled {
mi := &file_waCert_WACert_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CertChain_NoiseCertificate) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CertChain_NoiseCertificate) ProtoMessage() {}
func (x *CertChain_NoiseCertificate) ProtoReflect() protoreflect.Message {
mi := &file_waCert_WACert_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CertChain_NoiseCertificate.ProtoReflect.Descriptor instead.
func (*CertChain_NoiseCertificate) Descriptor() ([]byte, []int) {
return file_waCert_WACert_proto_rawDescGZIP(), []int{1, 0}
}
func (x *CertChain_NoiseCertificate) GetDetails() []byte {
if x != nil {
return x.Details
}
return nil
}
func (x *CertChain_NoiseCertificate) GetSignature() []byte {
if x != nil {
return x.Signature
}
return nil
}
type CertChain_NoiseCertificate_Details struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Serial uint32 `protobuf:"varint,1,opt,name=serial,proto3" json:"serial,omitempty"`
IssuerSerial uint32 `protobuf:"varint,2,opt,name=issuerSerial,proto3" json:"issuerSerial,omitempty"`
Key []byte `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"`
NotBefore uint64 `protobuf:"varint,4,opt,name=notBefore,proto3" json:"notBefore,omitempty"`
NotAfter uint64 `protobuf:"varint,5,opt,name=notAfter,proto3" json:"notAfter,omitempty"`
}
func (x *CertChain_NoiseCertificate_Details) Reset() {
*x = CertChain_NoiseCertificate_Details{}
if protoimpl.UnsafeEnabled {
mi := &file_waCert_WACert_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CertChain_NoiseCertificate_Details) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CertChain_NoiseCertificate_Details) ProtoMessage() {}
func (x *CertChain_NoiseCertificate_Details) ProtoReflect() protoreflect.Message {
mi := &file_waCert_WACert_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CertChain_NoiseCertificate_Details.ProtoReflect.Descriptor instead.
func (*CertChain_NoiseCertificate_Details) Descriptor() ([]byte, []int) {
return file_waCert_WACert_proto_rawDescGZIP(), []int{1, 0, 0}
}
func (x *CertChain_NoiseCertificate_Details) GetSerial() uint32 {
if x != nil {
return x.Serial
}
return 0
}
func (x *CertChain_NoiseCertificate_Details) GetIssuerSerial() uint32 {
if x != nil {
return x.IssuerSerial
}
return 0
}
func (x *CertChain_NoiseCertificate_Details) GetKey() []byte {
if x != nil {
return x.Key
}
return nil
}
func (x *CertChain_NoiseCertificate_Details) GetNotBefore() uint64 {
if x != nil {
return x.NotBefore
}
return 0
}
func (x *CertChain_NoiseCertificate_Details) GetNotAfter() uint64 {
if x != nil {
return x.NotAfter
}
return 0
}
var File_waCert_WACert_proto protoreflect.FileDescriptor
//go:embed WACert.pb.raw
var file_waCert_WACert_proto_rawDesc []byte
var (
file_waCert_WACert_proto_rawDescOnce sync.Once
file_waCert_WACert_proto_rawDescData = file_waCert_WACert_proto_rawDesc
)
func file_waCert_WACert_proto_rawDescGZIP() []byte {
file_waCert_WACert_proto_rawDescOnce.Do(func() {
file_waCert_WACert_proto_rawDescData = protoimpl.X.CompressGZIP(file_waCert_WACert_proto_rawDescData)
})
return file_waCert_WACert_proto_rawDescData
}
var file_waCert_WACert_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_waCert_WACert_proto_goTypes = []interface{}{
(*NoiseCertificate)(nil), // 0: WACert.NoiseCertificate
(*CertChain)(nil), // 1: WACert.CertChain
(*NoiseCertificate_Details)(nil), // 2: WACert.NoiseCertificate.Details
(*CertChain_NoiseCertificate)(nil), // 3: WACert.CertChain.NoiseCertificate
(*CertChain_NoiseCertificate_Details)(nil), // 4: WACert.CertChain.NoiseCertificate.Details
}
var file_waCert_WACert_proto_depIdxs = []int32{
3, // 0: WACert.CertChain.leaf:type_name -> WACert.CertChain.NoiseCertificate
3, // 1: WACert.CertChain.intermediate:type_name -> WACert.CertChain.NoiseCertificate
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_waCert_WACert_proto_init() }
func file_waCert_WACert_proto_init() {
if File_waCert_WACert_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_waCert_WACert_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NoiseCertificate); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waCert_WACert_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CertChain); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waCert_WACert_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*NoiseCertificate_Details); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waCert_WACert_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CertChain_NoiseCertificate); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waCert_WACert_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CertChain_NoiseCertificate_Details); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_waCert_WACert_proto_rawDesc,
NumEnums: 0,
NumMessages: 5,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_waCert_WACert_proto_goTypes,
DependencyIndexes: file_waCert_WACert_proto_depIdxs,
MessageInfos: file_waCert_WACert_proto_msgTypes,
}.Build()
File_waCert_WACert_proto = out.File
file_waCert_WACert_proto_rawDesc = nil
file_waCert_WACert_proto_goTypes = nil
file_waCert_WACert_proto_depIdxs = nil
}

View File

@ -0,0 +1,23 @@
waCert/WACert.protoWACert"Ë
NoiseCertificate
details ( Rdetails
signature ( R signature
Details
serial ( Rserial
issuer ( Rissuer
expires (Rexpires
subject ( Rsubject
key ( Rkey"ì
CertChain6
leaf ( 2".WACert.CertChain.NoiseCertificateRleafF
intermediate ( 2".WACert.CertChain.NoiseCertificateR intermediateÞ
NoiseCertificate
details ( Rdetails
signature ( R signature
Details
serial ( Rserial"
issuerSerial ( R issuerSerial
key ( Rkey
notBefore (R notBefore
notAfter (RnotAfterB-Z+go.mau.fi/whatsmeow/binary/armadillo/waCertbproto3

View File

@ -0,0 +1,34 @@
syntax = "proto3";
package WACert;
option go_package = "go.mau.fi/whatsmeow/binary/armadillo/waCert";
message NoiseCertificate {
message Details {
uint32 serial = 1;
string issuer = 2;
uint64 expires = 3;
string subject = 4;
bytes key = 5;
}
bytes details = 1;
bytes signature = 2;
}
message CertChain {
message NoiseCertificate {
message Details {
uint32 serial = 1;
uint32 issuerSerial = 2;
bytes key = 3;
uint64 notBefore = 4;
uint64 notAfter = 5;
}
bytes details = 1;
bytes signature = 2;
}
NoiseCertificate leaf = 1;
NoiseCertificate intermediate = 2;
}

View File

@ -0,0 +1,498 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc v3.21.12
// source: waCommon/WACommon.proto
package waCommon
import (
reflect "reflect"
sync "sync"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
_ "embed"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type FutureProofBehavior int32
const (
FutureProofBehavior_PLACEHOLDER FutureProofBehavior = 0
FutureProofBehavior_NO_PLACEHOLDER FutureProofBehavior = 1
FutureProofBehavior_IGNORE FutureProofBehavior = 2
)
// Enum value maps for FutureProofBehavior.
var (
FutureProofBehavior_name = map[int32]string{
0: "PLACEHOLDER",
1: "NO_PLACEHOLDER",
2: "IGNORE",
}
FutureProofBehavior_value = map[string]int32{
"PLACEHOLDER": 0,
"NO_PLACEHOLDER": 1,
"IGNORE": 2,
}
)
func (x FutureProofBehavior) Enum() *FutureProofBehavior {
p := new(FutureProofBehavior)
*p = x
return p
}
func (x FutureProofBehavior) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (FutureProofBehavior) Descriptor() protoreflect.EnumDescriptor {
return file_waCommon_WACommon_proto_enumTypes[0].Descriptor()
}
func (FutureProofBehavior) Type() protoreflect.EnumType {
return &file_waCommon_WACommon_proto_enumTypes[0]
}
func (x FutureProofBehavior) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use FutureProofBehavior.Descriptor instead.
func (FutureProofBehavior) EnumDescriptor() ([]byte, []int) {
return file_waCommon_WACommon_proto_rawDescGZIP(), []int{0}
}
type Command_CommandType int32
const (
Command_COMMANDTYPE_UNKNOWN Command_CommandType = 0
Command_EVERYONE Command_CommandType = 1
Command_SILENT Command_CommandType = 2
Command_AI Command_CommandType = 3
)
// Enum value maps for Command_CommandType.
var (
Command_CommandType_name = map[int32]string{
0: "COMMANDTYPE_UNKNOWN",
1: "EVERYONE",
2: "SILENT",
3: "AI",
}
Command_CommandType_value = map[string]int32{
"COMMANDTYPE_UNKNOWN": 0,
"EVERYONE": 1,
"SILENT": 2,
"AI": 3,
}
)
func (x Command_CommandType) Enum() *Command_CommandType {
p := new(Command_CommandType)
*p = x
return p
}
func (x Command_CommandType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Command_CommandType) Descriptor() protoreflect.EnumDescriptor {
return file_waCommon_WACommon_proto_enumTypes[1].Descriptor()
}
func (Command_CommandType) Type() protoreflect.EnumType {
return &file_waCommon_WACommon_proto_enumTypes[1]
}
func (x Command_CommandType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Command_CommandType.Descriptor instead.
func (Command_CommandType) EnumDescriptor() ([]byte, []int) {
return file_waCommon_WACommon_proto_rawDescGZIP(), []int{1, 0}
}
type MessageKey struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RemoteJID string `protobuf:"bytes,1,opt,name=remoteJID,proto3" json:"remoteJID,omitempty"`
FromMe bool `protobuf:"varint,2,opt,name=fromMe,proto3" json:"fromMe,omitempty"`
ID string `protobuf:"bytes,3,opt,name=ID,proto3" json:"ID,omitempty"`
Participant string `protobuf:"bytes,4,opt,name=participant,proto3" json:"participant,omitempty"`
}
func (x *MessageKey) Reset() {
*x = MessageKey{}
if protoimpl.UnsafeEnabled {
mi := &file_waCommon_WACommon_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MessageKey) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MessageKey) ProtoMessage() {}
func (x *MessageKey) ProtoReflect() protoreflect.Message {
mi := &file_waCommon_WACommon_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MessageKey.ProtoReflect.Descriptor instead.
func (*MessageKey) Descriptor() ([]byte, []int) {
return file_waCommon_WACommon_proto_rawDescGZIP(), []int{0}
}
func (x *MessageKey) GetRemoteJID() string {
if x != nil {
return x.RemoteJID
}
return ""
}
func (x *MessageKey) GetFromMe() bool {
if x != nil {
return x.FromMe
}
return false
}
func (x *MessageKey) GetID() string {
if x != nil {
return x.ID
}
return ""
}
func (x *MessageKey) GetParticipant() string {
if x != nil {
return x.Participant
}
return ""
}
type Command struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
CommandType Command_CommandType `protobuf:"varint,1,opt,name=commandType,proto3,enum=WACommon.Command_CommandType" json:"commandType,omitempty"`
Offset uint32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"`
Length uint32 `protobuf:"varint,3,opt,name=length,proto3" json:"length,omitempty"`
ValidationToken string `protobuf:"bytes,4,opt,name=validationToken,proto3" json:"validationToken,omitempty"`
}
func (x *Command) Reset() {
*x = Command{}
if protoimpl.UnsafeEnabled {
mi := &file_waCommon_WACommon_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Command) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Command) ProtoMessage() {}
func (x *Command) ProtoReflect() protoreflect.Message {
mi := &file_waCommon_WACommon_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Command.ProtoReflect.Descriptor instead.
func (*Command) Descriptor() ([]byte, []int) {
return file_waCommon_WACommon_proto_rawDescGZIP(), []int{1}
}
func (x *Command) GetCommandType() Command_CommandType {
if x != nil {
return x.CommandType
}
return Command_COMMANDTYPE_UNKNOWN
}
func (x *Command) GetOffset() uint32 {
if x != nil {
return x.Offset
}
return 0
}
func (x *Command) GetLength() uint32 {
if x != nil {
return x.Length
}
return 0
}
func (x *Command) GetValidationToken() string {
if x != nil {
return x.ValidationToken
}
return ""
}
type MessageText struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"`
MentionedJID []string `protobuf:"bytes,2,rep,name=mentionedJID,proto3" json:"mentionedJID,omitempty"`
Commands []*Command `protobuf:"bytes,3,rep,name=commands,proto3" json:"commands,omitempty"`
}
func (x *MessageText) Reset() {
*x = MessageText{}
if protoimpl.UnsafeEnabled {
mi := &file_waCommon_WACommon_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *MessageText) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MessageText) ProtoMessage() {}
func (x *MessageText) ProtoReflect() protoreflect.Message {
mi := &file_waCommon_WACommon_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MessageText.ProtoReflect.Descriptor instead.
func (*MessageText) Descriptor() ([]byte, []int) {
return file_waCommon_WACommon_proto_rawDescGZIP(), []int{2}
}
func (x *MessageText) GetText() string {
if x != nil {
return x.Text
}
return ""
}
func (x *MessageText) GetMentionedJID() []string {
if x != nil {
return x.MentionedJID
}
return nil
}
func (x *MessageText) GetCommands() []*Command {
if x != nil {
return x.Commands
}
return nil
}
type SubProtocol struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Payload []byte `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
Version int32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"`
}
func (x *SubProtocol) Reset() {
*x = SubProtocol{}
if protoimpl.UnsafeEnabled {
mi := &file_waCommon_WACommon_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *SubProtocol) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SubProtocol) ProtoMessage() {}
func (x *SubProtocol) ProtoReflect() protoreflect.Message {
mi := &file_waCommon_WACommon_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SubProtocol.ProtoReflect.Descriptor instead.
func (*SubProtocol) Descriptor() ([]byte, []int) {
return file_waCommon_WACommon_proto_rawDescGZIP(), []int{3}
}
func (x *SubProtocol) GetPayload() []byte {
if x != nil {
return x.Payload
}
return nil
}
func (x *SubProtocol) GetVersion() int32 {
if x != nil {
return x.Version
}
return 0
}
var File_waCommon_WACommon_proto protoreflect.FileDescriptor
//go:embed WACommon.pb.raw
var file_waCommon_WACommon_proto_rawDesc []byte
var (
file_waCommon_WACommon_proto_rawDescOnce sync.Once
file_waCommon_WACommon_proto_rawDescData = file_waCommon_WACommon_proto_rawDesc
)
func file_waCommon_WACommon_proto_rawDescGZIP() []byte {
file_waCommon_WACommon_proto_rawDescOnce.Do(func() {
file_waCommon_WACommon_proto_rawDescData = protoimpl.X.CompressGZIP(file_waCommon_WACommon_proto_rawDescData)
})
return file_waCommon_WACommon_proto_rawDescData
}
var file_waCommon_WACommon_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_waCommon_WACommon_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_waCommon_WACommon_proto_goTypes = []interface{}{
(FutureProofBehavior)(0), // 0: WACommon.FutureProofBehavior
(Command_CommandType)(0), // 1: WACommon.Command.CommandType
(*MessageKey)(nil), // 2: WACommon.MessageKey
(*Command)(nil), // 3: WACommon.Command
(*MessageText)(nil), // 4: WACommon.MessageText
(*SubProtocol)(nil), // 5: WACommon.SubProtocol
}
var file_waCommon_WACommon_proto_depIdxs = []int32{
1, // 0: WACommon.Command.commandType:type_name -> WACommon.Command.CommandType
3, // 1: WACommon.MessageText.commands:type_name -> WACommon.Command
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_waCommon_WACommon_proto_init() }
func file_waCommon_WACommon_proto_init() {
if File_waCommon_WACommon_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_waCommon_WACommon_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*MessageKey); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waCommon_WACommon_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Command); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waCommon_WACommon_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*MessageText); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_waCommon_WACommon_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*SubProtocol); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_waCommon_WACommon_proto_rawDesc,
NumEnums: 2,
NumMessages: 4,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_waCommon_WACommon_proto_goTypes,
DependencyIndexes: file_waCommon_WACommon_proto_depIdxs,
EnumInfos: file_waCommon_WACommon_proto_enumTypes,
MessageInfos: file_waCommon_WACommon_proto_msgTypes,
}.Build()
File_waCommon_WACommon_proto = out.File
file_waCommon_WACommon_proto_rawDesc = nil
file_waCommon_WACommon_proto_goTypes = nil
file_waCommon_WACommon_proto_depIdxs = nil
}

View File

@ -0,0 +1,41 @@
syntax = "proto3";
package WACommon;
option go_package = "go.mau.fi/whatsmeow/binary/armadillo/waCommon";
enum FutureProofBehavior {
PLACEHOLDER = 0;
NO_PLACEHOLDER = 1;
IGNORE = 2;
}
message MessageKey {
string remoteJID = 1;
bool fromMe = 2;
string ID = 3;
string participant = 4;
}
message Command {
enum CommandType {
COMMANDTYPE_UNKNOWN = 0;
EVERYONE = 1;
SILENT = 2;
AI = 3;
}
CommandType commandType = 1;
uint32 offset = 2;
uint32 length = 3;
string validationToken = 4;
}
message MessageText {
string text = 1;
repeated string mentionedJID = 2;
repeated Command commands = 3;
}
message SubProtocol {
bytes payload = 1;
int32 version = 2;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,234 @@
syntax = "proto3";
package WAConsumerApplication;
option go_package = "go.mau.fi/whatsmeow/binary/armadillo/waConsumerApplication";
import "waCommon/WACommon.proto";
message ConsumerApplication {
message Payload {
oneof payload {
Content content = 1;
ApplicationData applicationData = 2;
Signal signal = 3;
SubProtocolPayload subProtocol = 4;
}
}
message SubProtocolPayload {
WACommon.FutureProofBehavior futureProof = 1;
}
message Metadata {
enum SpecialTextSize {
SPECIALTEXTSIZE_UNKNOWN = 0;
SMALL = 1;
MEDIUM = 2;
LARGE = 3;
}
SpecialTextSize specialTextSize = 1;
}
message Signal {
}
message ApplicationData {
oneof applicationContent {
RevokeMessage revoke = 1;
}
}
message Content {
oneof content {
WACommon.MessageText messageText = 1;
ImageMessage imageMessage = 2;
ContactMessage contactMessage = 3;
LocationMessage locationMessage = 4;
ExtendedTextMessage extendedTextMessage = 5;
StatusTextMesage statusTextMessage = 6;
DocumentMessage documentMessage = 7;
AudioMessage audioMessage = 8;
VideoMessage videoMessage = 9;
ContactsArrayMessage contactsArrayMessage = 10;
LiveLocationMessage liveLocationMessage = 11;
StickerMessage stickerMessage = 12;
GroupInviteMessage groupInviteMessage = 13;
ViewOnceMessage viewOnceMessage = 14;
ReactionMessage reactionMessage = 16;
PollCreationMessage pollCreationMessage = 17;
PollUpdateMessage pollUpdateMessage = 18;
EditMessage editMessage = 19;
}
}
message EditMessage {
WACommon.MessageKey key = 1;
WACommon.MessageText message = 2;
int64 timestampMS = 3;
}
message PollAddOptionMessage {
repeated Option pollOption = 1;
}
message PollVoteMessage {
repeated bytes selectedOptions = 1;
int64 senderTimestampMS = 2;
}
message PollEncValue {
bytes encPayload = 1;
bytes encIV = 2;
}
message PollUpdateMessage {
WACommon.MessageKey pollCreationMessageKey = 1;
PollEncValue vote = 2;
PollEncValue addOption = 3;
}
message PollCreationMessage {
bytes encKey = 1;
string name = 2;
repeated Option options = 3;
uint32 selectableOptionsCount = 4;
}
message Option {
string optionName = 1;
}
message ReactionMessage {
WACommon.MessageKey key = 1;
string text = 2;
string groupingKey = 3;
int64 senderTimestampMS = 4;
string reactionMetadataDataclassData = 5;
int32 style = 6;
}
message RevokeMessage {
WACommon.MessageKey key = 1;
}
message ViewOnceMessage {
oneof viewOnceContent {
ImageMessage imageMessage = 1;
VideoMessage videoMessage = 2;
}
}
message GroupInviteMessage {
string groupJID = 1;
string inviteCode = 2;
int64 inviteExpiration = 3;
string groupName = 4;
bytes JPEGThumbnail = 5;
WACommon.MessageText caption = 6;
}
message LiveLocationMessage {
Location location = 1;
uint32 accuracyInMeters = 2;
float speedInMps = 3;
uint32 degreesClockwiseFromMagneticNorth = 4;
WACommon.MessageText caption = 5;
int64 sequenceNumber = 6;
uint32 timeOffset = 7;
}
message ContactsArrayMessage {
string displayName = 1;
repeated ContactMessage contacts = 2;
}
message ContactMessage {
WACommon.SubProtocol contact = 1;
}
message StatusTextMesage {
enum FontType {
SANS_SERIF = 0;
SERIF = 1;
NORICAN_REGULAR = 2;
BRYNDAN_WRITE = 3;
BEBASNEUE_REGULAR = 4;
OSWALD_HEAVY = 5;
}
ExtendedTextMessage text = 1;
fixed32 textArgb = 6;
fixed32 backgroundArgb = 7;
FontType font = 8;
}
message ExtendedTextMessage {
enum PreviewType {
NONE = 0;
VIDEO = 1;
}
WACommon.MessageText text = 1;
string matchedText = 2;
string canonicalURL = 3;
string description = 4;
string title = 5;
WACommon.SubProtocol thumbnail = 6;
PreviewType previewType = 7;
}
message LocationMessage {
Location location = 1;
string address = 2;
}
message StickerMessage {
WACommon.SubProtocol sticker = 1;
}
message DocumentMessage {
WACommon.SubProtocol document = 1;
string fileName = 2;
}
message VideoMessage {
WACommon.SubProtocol video = 1;
WACommon.MessageText caption = 2;
}
message AudioMessage {
WACommon.SubProtocol audio = 1;
bool PTT = 2;
}
message ImageMessage {
WACommon.SubProtocol image = 1;
WACommon.MessageText caption = 2;
}
message InteractiveAnnotation {
oneof action {
Location location = 2;
}
repeated Point polygonVertices = 1;
}
message Point {
double x = 1;
double y = 2;
}
message Location {
double degreesLatitude = 1;
double degreesLongitude = 2;
string name = 3;
}
message MediaPayload {
WACommon.SubProtocol protocol = 1;
}
Payload payload = 1;
Metadata metadata = 2;
}

View File

@ -0,0 +1,82 @@
package waConsumerApplication
import (
"go.mau.fi/whatsmeow/binary/armadillo/armadilloutil"
"go.mau.fi/whatsmeow/binary/armadillo/waMediaTransport"
)
type ConsumerApplication_Content_Content = isConsumerApplication_Content_Content
func (*ConsumerApplication) IsMessageApplicationSub() {}
const (
ImageTransportVersion = 1
StickerTransportVersion = 1
VideoTransportVersion = 1
AudioTransportVersion = 1
DocumentTransportVersion = 1
ContactTransportVersion = 1
)
func (msg *ConsumerApplication_ImageMessage) Decode() (dec *waMediaTransport.ImageTransport, err error) {
return armadilloutil.Unmarshal(&waMediaTransport.ImageTransport{}, msg.GetImage(), ImageTransportVersion)
}
func (msg *ConsumerApplication_ImageMessage) Set(payload *waMediaTransport.ImageTransport) (err error) {
msg.Image, err = armadilloutil.Marshal(payload, ImageTransportVersion)
return
}
func (msg *ConsumerApplication_StickerMessage) Decode() (dec *waMediaTransport.StickerTransport, err error) {
return armadilloutil.Unmarshal(&waMediaTransport.StickerTransport{}, msg.GetSticker(), StickerTransportVersion)
}
func (msg *ConsumerApplication_StickerMessage) Set(payload *waMediaTransport.StickerTransport) (err error) {
msg.Sticker, err = armadilloutil.Marshal(payload, StickerTransportVersion)
return
}
func (msg *ConsumerApplication_ExtendedTextMessage) DecodeThumbnail() (dec *waMediaTransport.ImageTransport, err error) {
return armadilloutil.Unmarshal(&waMediaTransport.ImageTransport{}, msg.GetThumbnail(), ImageTransportVersion)
}
func (msg *ConsumerApplication_ExtendedTextMessage) SetThumbnail(payload *waMediaTransport.ImageTransport) (err error) {
msg.Thumbnail, err = armadilloutil.Marshal(payload, ImageTransportVersion)
return
}
func (msg *ConsumerApplication_VideoMessage) Decode() (dec *waMediaTransport.VideoTransport, err error) {
return armadilloutil.Unmarshal(&waMediaTransport.VideoTransport{}, msg.GetVideo(), VideoTransportVersion)
}
func (msg *ConsumerApplication_VideoMessage) Set(payload *waMediaTransport.VideoTransport) (err error) {
msg.Video, err = armadilloutil.Marshal(payload, VideoTransportVersion)
return
}
func (msg *ConsumerApplication_AudioMessage) Decode() (dec *waMediaTransport.AudioTransport, err error) {
return armadilloutil.Unmarshal(&waMediaTransport.AudioTransport{}, msg.GetAudio(), AudioTransportVersion)
}
func (msg *ConsumerApplication_AudioMessage) Set(payload *waMediaTransport.AudioTransport) (err error) {
msg.Audio, err = armadilloutil.Marshal(payload, AudioTransportVersion)
return
}
func (msg *ConsumerApplication_DocumentMessage) Decode() (dec *waMediaTransport.DocumentTransport, err error) {
return armadilloutil.Unmarshal(&waMediaTransport.DocumentTransport{}, msg.GetDocument(), DocumentTransportVersion)
}
func (msg *ConsumerApplication_DocumentMessage) Set(payload *waMediaTransport.DocumentTransport) (err error) {
msg.Document, err = armadilloutil.Marshal(payload, DocumentTransportVersion)
return
}
func (msg *ConsumerApplication_ContactMessage) Decode() (dec *waMediaTransport.ContactTransport, err error) {
return armadilloutil.Unmarshal(&waMediaTransport.ContactTransport{}, msg.GetContact(), ContactTransportVersion)
}
func (msg *ConsumerApplication_ContactMessage) Set(payload *waMediaTransport.ContactTransport) (err error) {
msg.Contact, err = armadilloutil.Marshal(payload, ContactTransportVersion)
return
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -59,6 +59,24 @@ func (cli *Client) handleCallEvent(node *waBinary.Node) {
},
Data: &child,
})
case "preaccept":
cli.dispatchEvent(&events.CallPreAccept{
BasicCallMeta: basicMeta,
CallRemoteMeta: types.CallRemoteMeta{
RemotePlatform: ag.String("platform"),
RemoteVersion: ag.String("version"),
},
Data: &child,
})
case "transport":
cli.dispatchEvent(&events.CallTransport{
BasicCallMeta: basicMeta,
CallRemoteMeta: types.CallRemoteMeta{
RemotePlatform: ag.String("platform"),
RemoteVersion: ag.String("version"),
},
Data: &child,
})
case "terminate":
cli.dispatchEvent(&events.CallTerminate{
BasicCallMeta: basicMeta,

View File

@ -19,6 +19,10 @@ import (
"sync/atomic"
"time"
"github.com/gorilla/websocket"
"go.mau.fi/util/random"
"golang.org/x/net/proxy"
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
@ -28,7 +32,6 @@ import (
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/keys"
waLog "go.mau.fi/whatsmeow/util/log"
"go.mau.fi/whatsmeow/util/randbytes"
)
// EventHandler is a function that can handle events from WhatsApp.
@ -42,6 +45,11 @@ type wrappedEventHandler struct {
id uint32
}
type deviceCache struct {
devices []types.JID
dhash string
}
// Client contains everything necessary to connect to and interact with the WhatsApp web API.
type Client struct {
Store *store.Device
@ -53,13 +61,16 @@ type Client struct {
socketLock sync.RWMutex
socketWait chan struct{}
isLoggedIn uint32
expectedDisconnectVal uint32
isLoggedIn atomic.Bool
expectedDisconnect atomic.Bool
EnableAutoReconnect bool
LastSuccessfulConnect time.Time
AutoReconnectErrors int
// AutoReconnectHook is called when auto-reconnection fails. If the function returns false,
// the client will not attempt to reconnect. The number of retries can be read from AutoReconnectErrors.
AutoReconnectHook func(error) bool
sendActiveReceipts uint32
sendActiveReceipts atomic.Uint32
// EmitAppStateEventsOnFullSync can be set to true if you want to get app state events emitted
// even when re-syncing the whole state.
@ -73,7 +84,7 @@ type Client struct {
appStateSyncLock sync.Mutex
historySyncNotifications chan *waProto.HistorySyncNotification
historySyncHandlerStarted uint32
historySyncHandlerStarted atomic.Bool
uploadPreKeysLock sync.Mutex
lastPreKeyUpload time.Time
@ -92,6 +103,9 @@ type Client struct {
messageRetries map[string]int
messageRetriesLock sync.Mutex
incomingRetryRequestCounter map[incomingRetryKey]int
incomingRetryRequestCounterLock sync.Mutex
appStateKeyRequests map[string]time.Time
appStateKeyRequestsLock sync.RWMutex
@ -101,10 +115,10 @@ type Client struct {
groupParticipantsCache map[types.JID][]types.JID
groupParticipantsCacheLock sync.Mutex
userDevicesCache map[types.JID][]types.JID
userDevicesCache map[types.JID]deviceCache
userDevicesCacheLock sync.Mutex
recentMessagesMap map[recentMessageKey]*waProto.Message
recentMessagesMap map[recentMessageKey]RecentMessage
recentMessagesList [recentMessagesSize]recentMessageKey
recentMessagesPtr int
recentMessagesLock sync.RWMutex
@ -122,6 +136,10 @@ type Client struct {
// the client will disconnect.
PrePairCallback func(jid types.JID, platform, businessName string) bool
// GetClientPayload is called to get the client payload for connecting to the server.
// This should NOT be used for WhatsApp (to change the OS name, update fields in store.BaseClientPayload directly).
GetClientPayload func() *waProto.ClientPayload
// Should untrusted identity errors be handled automatically? If true, the stored identity and existing signal
// sessions will be removed on untrusted identity errors, and an events.IdentityChange will be dispatched.
// If false, decrypting a message from untrusted devices will fail.
@ -137,10 +155,25 @@ type Client struct {
phoneLinkingCache *phoneLinkingCache
uniqueID string
idCounter uint32
idCounter atomic.Uint64
proxy socket.Proxy
http *http.Client
proxy Proxy
socksProxy proxy.Dialer
proxyOnlyLogin bool
http *http.Client
// This field changes the client to act like a Messenger client instead of a WhatsApp one.
//
// Note that you cannot use a Messenger account just by setting this field, you must use a
// separate library for all the non-e2ee-related stuff like logging in.
// The library is currently embedded in mautrix-meta (https://github.com/mautrix/meta), but may be separated later.
MessengerConfig *MessengerConfig
RefreshCAT func() error
}
type MessengerConfig struct {
UserAgent string
BaseURL string
}
// Size of buffer for the channel that all incoming XML nodes go through.
@ -167,7 +200,7 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
if log == nil {
log = waLog.Noop
}
uniqueIDPrefix := randbytes.Make(2)
uniqueIDPrefix := random.Bytes(2)
cli := &Client{
http: &http.Client{
Transport: (http.DefaultTransport.(*http.Transport)).Clone(),
@ -185,12 +218,14 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
appStateProc: appstate.NewProcessor(deviceStore, log.Sub("AppState")),
socketWait: make(chan struct{}),
incomingRetryRequestCounter: make(map[incomingRetryKey]int),
historySyncNotifications: make(chan *waProto.HistorySyncNotification, 32),
groupParticipantsCache: make(map[types.JID][]types.JID),
userDevicesCache: make(map[types.JID][]types.JID),
userDevicesCache: make(map[types.JID]deviceCache),
recentMessagesMap: make(map[recentMessageKey]*waProto.Message, recentMessagesSize),
recentMessagesMap: make(map[recentMessageKey]RecentMessage, recentMessagesSize),
sessionRecreateHistory: make(map[types.JID]time.Time),
GetMessageForRetry: func(requester, to types.JID, id types.MessageID) *waProto.Message { return nil },
appStateKeyRequests: make(map[string]time.Time),
@ -218,19 +253,35 @@ func NewClient(deviceStore *store.Device, log waLog.Logger) *Client {
return cli
}
// SetProxyAddress is a helper method that parses a URL string and calls SetProxy.
// SetProxyAddress is a helper method that parses a URL string and calls SetProxy or SetSOCKSProxy based on the URL scheme.
//
// Returns an error if url.Parse fails to parse the given address.
func (cli *Client) SetProxyAddress(addr string) error {
func (cli *Client) SetProxyAddress(addr string, opts ...SetProxyOptions) error {
if addr == "" {
cli.SetProxy(nil, opts...)
return nil
}
parsed, err := url.Parse(addr)
if err != nil {
return err
}
cli.SetProxy(http.ProxyURL(parsed))
if parsed.Scheme == "http" || parsed.Scheme == "https" {
cli.SetProxy(http.ProxyURL(parsed), opts...)
} else if parsed.Scheme == "socks5" {
px, err := proxy.FromURL(parsed, proxy.Direct)
if err != nil {
return err
}
cli.SetSOCKSProxy(px, opts...)
} else {
return fmt.Errorf("unsupported proxy scheme %q", parsed.Scheme)
}
return nil
}
// SetProxy sets the proxy to use for WhatsApp web websocket connections and media uploads/downloads.
type Proxy = func(*http.Request) (*url.URL, error)
// SetProxy sets a HTTP proxy to use for WhatsApp web websocket connections and media uploads/downloads.
//
// Must be called before Connect() to take effect in the websocket connection.
// If you want to change the proxy after connecting, you must call Disconnect() and then Connect() again manually.
@ -250,9 +301,59 @@ func (cli *Client) SetProxyAddress(addr string) error {
// return mediaProxyURL, nil
// }
// })
func (cli *Client) SetProxy(proxy socket.Proxy) {
cli.proxy = proxy
cli.http.Transport.(*http.Transport).Proxy = proxy
func (cli *Client) SetProxy(proxy Proxy, opts ...SetProxyOptions) {
var opt SetProxyOptions
if len(opts) > 0 {
opt = opts[0]
}
if !opt.NoWebsocket {
cli.proxy = proxy
cli.socksProxy = nil
}
if !opt.NoMedia {
transport := cli.http.Transport.(*http.Transport)
transport.Proxy = proxy
transport.Dial = nil
transport.DialContext = nil
}
}
type SetProxyOptions struct {
// If NoWebsocket is true, the proxy won't be used for the websocket
NoWebsocket bool
// If NoMedia is true, the proxy won't be used for media uploads/downloads
NoMedia bool
}
// SetSOCKSProxy sets a SOCKS5 proxy to use for WhatsApp web websocket connections and media uploads/downloads.
//
// Same details as SetProxy apply, but using a different proxy for the websocket and media is not currently supported.
func (cli *Client) SetSOCKSProxy(px proxy.Dialer, opts ...SetProxyOptions) {
var opt SetProxyOptions
if len(opts) > 0 {
opt = opts[0]
}
if !opt.NoWebsocket {
cli.socksProxy = px
cli.proxy = nil
}
if !opt.NoMedia {
transport := cli.http.Transport.(*http.Transport)
transport.Proxy = nil
transport.Dial = cli.socksProxy.Dial
contextDialer, ok := cli.socksProxy.(proxy.ContextDialer)
if ok {
transport.DialContext = contextDialer.DialContext
} else {
transport.DialContext = nil
}
}
}
// ToggleProxyOnlyForLogin changes whether the proxy set with SetProxy or related methods
// is only used for the pre-login websocket and not authenticated websockets.
func (cli *Client) ToggleProxyOnlyForLogin(only bool) {
cli.proxyOnlyLogin = only
}
func (cli *Client) getSocketWaitChan() <-chan struct{} {
@ -308,7 +409,27 @@ func (cli *Client) Connect() error {
}
cli.resetExpectedDisconnect()
fs := socket.NewFrameSocket(cli.Log.Sub("Socket"), socket.WAConnHeader, cli.proxy)
wsDialer := websocket.Dialer{}
if !cli.proxyOnlyLogin || cli.Store.ID == nil {
if cli.proxy != nil {
wsDialer.Proxy = cli.proxy
} else if cli.socksProxy != nil {
wsDialer.NetDial = cli.socksProxy.Dial
contextDialer, ok := cli.socksProxy.(proxy.ContextDialer)
if ok {
wsDialer.NetDialContext = contextDialer.DialContext
}
}
}
fs := socket.NewFrameSocket(cli.Log.Sub("Socket"), wsDialer)
if cli.MessengerConfig != nil {
fs.URL = "wss://web-chat-e2ee.facebook.com/ws/chat"
fs.HTTPHeaders.Set("Origin", cli.MessengerConfig.BaseURL)
fs.HTTPHeaders.Set("User-Agent", cli.MessengerConfig.UserAgent)
fs.HTTPHeaders.Set("Sec-Fetch-Dest", "empty")
fs.HTTPHeaders.Set("Sec-Fetch-Mode", "websocket")
fs.HTTPHeaders.Set("Sec-Fetch-Site", "cross-site")
}
if err := fs.Connect(); err != nil {
fs.Close(0)
return err
@ -323,7 +444,7 @@ func (cli *Client) Connect() error {
// IsLoggedIn returns true after the client is successfully connected and authenticated on WhatsApp.
func (cli *Client) IsLoggedIn() bool {
return atomic.LoadUint32(&cli.isLoggedIn) == 1
return cli.isLoggedIn.Load()
}
func (cli *Client) onDisconnect(ns *socket.NoiseSocket, remote bool) {
@ -348,15 +469,15 @@ func (cli *Client) onDisconnect(ns *socket.NoiseSocket, remote bool) {
}
func (cli *Client) expectDisconnect() {
atomic.StoreUint32(&cli.expectedDisconnectVal, 1)
cli.expectedDisconnect.Store(true)
}
func (cli *Client) resetExpectedDisconnect() {
atomic.StoreUint32(&cli.expectedDisconnectVal, 0)
cli.expectedDisconnect.Store(false)
}
func (cli *Client) isExpectedDisconnect() bool {
return atomic.LoadUint32(&cli.expectedDisconnectVal) == 1
return cli.expectedDisconnect.Load()
}
func (cli *Client) autoReconnect() {
@ -374,6 +495,10 @@ func (cli *Client) autoReconnect() {
return
} else if err != nil {
cli.Log.Errorf("Error reconnecting after autoreconnect sleep: %v", err)
if cli.AutoReconnectHook != nil && !cli.AutoReconnectHook(err) {
cli.Log.Debugf("AutoReconnectHook returned false, not reconnecting")
return
}
} else {
return
}
@ -419,6 +544,9 @@ func (cli *Client) unlockedDisconnect() {
// Note that this will not emit any events. The LoggedOut event is only used for external logouts
// (triggered by the user from the main device or by WhatsApp servers).
func (cli *Client) Logout() error {
if cli.MessengerConfig != nil {
return errors.New("can't logout with Messenger credentials")
}
ownID := cli.getOwnID()
if ownID.IsEmpty() {
return ErrNotLoggedIn
@ -667,7 +795,7 @@ func (cli *Client) ParseWebMessage(chatJID types.JID, webMsg *waProto.WebMessage
if info.Sender.IsEmpty() {
return nil, ErrNotLoggedIn
}
} else if chatJID.Server == types.DefaultUserServer {
} else if chatJID.Server == types.DefaultUserServer || chatJID.Server == types.NewsletterServer {
info.Sender = chatJID
} else if webMsg.GetParticipant() != "" {
info.Sender, err = types.ParseJID(webMsg.GetParticipant())

View File

@ -0,0 +1,76 @@
// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow_test
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log"
)
func eventHandler(evt interface{}) {
switch v := evt.(type) {
case *events.Message:
fmt.Println("Received a message!", v.Message.GetConversation())
}
}
func Example() {
dbLog := waLog.Stdout("Database", "DEBUG", true)
// Make sure you add appropriate DB connector imports, e.g. github.com/mattn/go-sqlite3 for SQLite
container, err := sqlstore.New("sqlite3", "file:examplestore.db?_foreign_keys=on", dbLog)
if err != nil {
panic(err)
}
// If you want multiple sessions, remember their JIDs and use .GetDevice(jid) or .GetAllDevices() instead.
deviceStore, err := container.GetFirstDevice()
if err != nil {
panic(err)
}
clientLog := waLog.Stdout("Client", "DEBUG", true)
client := whatsmeow.NewClient(deviceStore, clientLog)
client.AddEventHandler(eventHandler)
if client.Store.ID == nil {
// No ID stored, new login
qrChan, _ := client.GetQRChannel(context.Background())
err = client.Connect()
if err != nil {
panic(err)
}
for evt := range qrChan {
if evt.Event == "code" {
// Render the QR code here
// e.g. qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
// or just manually `echo 2@... | qrencode -t ansiutf8` in a terminal
fmt.Println("QR code:", evt.Code)
} else {
fmt.Println("Login event:", evt.Event)
}
}
} else {
// Already logged in, just connect
err = client.Connect()
if err != nil {
panic(err)
}
}
// Listen to Ctrl+C (you can also do something else that prevents the program from exiting)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c
client.Disconnect()
}

View File

@ -7,7 +7,6 @@
package whatsmeow
import (
"sync/atomic"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
@ -17,7 +16,7 @@ import (
)
func (cli *Client) handleStreamError(node *waBinary.Node) {
atomic.StoreUint32(&cli.isLoggedIn, 0)
cli.isLoggedIn.Store(false)
cli.clearResponseWaiters(node)
code, _ := node.Attrs["code"].(string)
conflict, _ := node.GetOptionalChildByTag("conflict")
@ -48,6 +47,16 @@ func (cli *Client) handleStreamError(node *waBinary.Node) {
// This seems to happen when the server wants to restart or something.
// The disconnection will be emitted as an events.Disconnected and then the auto-reconnect will do its thing.
cli.Log.Warnf("Got 503 stream error, assuming automatic reconnect will handle it")
case cli.RefreshCAT != nil && (code == events.ConnectFailureCATInvalid.NumberString() || code == events.ConnectFailureCATExpired.NumberString()):
cli.Log.Infof("Got %s stream error, refreshing CAT before reconnecting...", code)
cli.socketLock.RLock()
defer cli.socketLock.RUnlock()
err := cli.RefreshCAT()
if err != nil {
cli.Log.Errorf("Failed to refresh CAT: %v", err)
cli.expectDisconnect()
go cli.dispatchEvent(&events.CATRefreshError{Error: err})
}
default:
cli.Log.Errorf("Unknown stream error: %s", node.XMLString())
go cli.dispatchEvent(&events.StreamError{Code: code, Raw: node})
@ -89,9 +98,20 @@ func (cli *Client) handleConnectFailure(node *waBinary.Node) {
willAutoReconnect = false
case reason == events.ConnectFailureServiceUnavailable:
// Auto-reconnect for 503s
case reason == events.ConnectFailureCATInvalid || reason == events.ConnectFailureCATExpired:
// Auto-reconnect when rotating CAT, lock socket to ensure refresh goes through before reconnect
cli.socketLock.RLock()
defer cli.socketLock.RUnlock()
case reason == 500 && message == "biz vname fetch error":
// These happen for business accounts randomly, also auto-reconnect
}
if reason == 403 {
cli.Log.Debugf(
"Message for 403 connect failure: %s / %s",
ag.OptionalString("logout_message_header"),
ag.OptionalString("logout_message_subtext"),
)
}
if reason.IsLoggedOut() {
cli.Log.Infof("Got %s connect failure, sending LoggedOut event and deleting session", reason)
go cli.dispatchEvent(&events.LoggedOut{OnConnect: true, Reason: reason})
@ -108,6 +128,14 @@ func (cli *Client) handleConnectFailure(node *waBinary.Node) {
} else if reason == events.ConnectFailureClientOutdated {
cli.Log.Errorf("Client outdated (405) connect failure (client version: %s)", store.GetWAVersion().String())
go cli.dispatchEvent(&events.ClientOutdated{})
} else if reason == events.ConnectFailureCATInvalid || reason == events.ConnectFailureCATExpired {
cli.Log.Infof("Got %d/%s connect failure, refreshing CAT before reconnecting...", int(reason), message)
err := cli.RefreshCAT()
if err != nil {
cli.Log.Errorf("Failed to refresh CAT: %v", err)
cli.expectDisconnect()
go cli.dispatchEvent(&events.CATRefreshError{Error: err})
}
} else if willAutoReconnect {
cli.Log.Warnf("Got %d/%s connect failure, assuming automatic reconnect will handle it", int(reason), message)
} else {
@ -120,7 +148,7 @@ func (cli *Client) handleConnectSuccess(node *waBinary.Node) {
cli.Log.Infof("Successfully authenticated")
cli.LastSuccessfulConnect = time.Now()
cli.AutoReconnectErrors = 0
atomic.StoreUint32(&cli.isLoggedIn, 1)
cli.isLoggedIn.Store(true)
go func() {
if dbCount, err := cli.Store.PreKeys.UploadedPreKeyCount(); err != nil {
cli.Log.Errorf("Failed to get number of prekeys in database: %v", err)

View File

@ -10,6 +10,7 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
@ -17,9 +18,11 @@ import (
"strings"
"time"
"go.mau.fi/util/retryafter"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"go.mau.fi/whatsmeow/binary/armadillo/waMediaTransport"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/socket"
"go.mau.fi/whatsmeow/util/cbcutil"
@ -208,6 +211,10 @@ func (cli *Client) Download(msg DownloadableMessage) ([]byte, error) {
}
}
func (cli *Client) DownloadFB(transport *waMediaTransport.WAMediaTransport_Integral, mediaType MediaType) ([]byte, error) {
return cli.DownloadMediaWithPath(transport.GetDirectPath(), transport.GetFileEncSHA256(), transport.GetFileSHA256(), transport.GetMediaKey(), -1, mediaType, mediaTypeToMMSType[mediaType])
}
// DownloadMediaWithPath downloads an attachment by manually specifying the path and encryption details.
func (cli *Client) DownloadMediaWithPath(directPath string, encFileHash, fileHash, mediaKey []byte, fileLength int, mediaType MediaType, mmsType string) (data []byte, err error) {
var mediaConn *MediaConn
@ -219,15 +226,16 @@ func (cli *Client) DownloadMediaWithPath(directPath string, encFileHash, fileHas
mmsType = mediaTypeToMMSType[mediaType]
}
for i, host := range mediaConn.Hosts {
// TODO omit hash for unencrypted media?
mediaURL := fmt.Sprintf("https://%s%s&hash=%s&mms-type=%s&__wa-mms=", host.Hostname, directPath, base64.URLEncoding.EncodeToString(encFileHash), mmsType)
data, err = cli.downloadAndDecrypt(mediaURL, mediaKey, mediaType, fileLength, encFileHash, fileHash)
// TODO there are probably some errors that shouldn't retry
if err != nil {
if i >= len(mediaConn.Hosts)-1 {
return nil, fmt.Errorf("failed to download media from last host: %w", err)
}
cli.Log.Warnf("Failed to download media: %s, trying with next host...", err)
if err == nil {
return
} else if i >= len(mediaConn.Hosts)-1 {
return nil, fmt.Errorf("failed to download media from last host: %w", err)
}
// TODO there are probably some errors that shouldn't retry
cli.Log.Warnf("Failed to download media: %s, trying with next host...", err)
}
return
}
@ -235,8 +243,11 @@ func (cli *Client) DownloadMediaWithPath(directPath string, encFileHash, fileHas
func (cli *Client) downloadAndDecrypt(url string, mediaKey []byte, appInfo MediaType, fileLength int, fileEncSha256, fileSha256 []byte) (data []byte, err error) {
iv, cipherKey, macKey, _ := getMediaKeys(mediaKey, appInfo)
var ciphertext, mac []byte
if ciphertext, mac, err = cli.downloadEncryptedMediaWithRetries(url, fileEncSha256); err != nil {
if ciphertext, mac, err = cli.downloadPossiblyEncryptedMediaWithRetries(url, fileEncSha256); err != nil {
} else if mediaKey == nil && fileEncSha256 == nil && mac == nil {
// Unencrypted media, just return the downloaded data
data = ciphertext
} else if err = validateMedia(iv, ciphertext, macKey, mac); err != nil {
} else if data, err = cbcutil.Decrypt(cipherKey, iv, ciphertext); err != nil {
@ -254,52 +265,59 @@ func getMediaKeys(mediaKey []byte, appInfo MediaType) (iv, cipherKey, macKey, re
return mediaKeyExpanded[:16], mediaKeyExpanded[16:48], mediaKeyExpanded[48:80], mediaKeyExpanded[80:]
}
func (cli *Client) downloadEncryptedMediaWithRetries(url string, checksum []byte) (file, mac []byte, err error) {
func shouldRetryMediaDownload(err error) bool {
var netErr net.Error
var httpErr DownloadHTTPError
return errors.As(err, &netErr) ||
strings.HasPrefix(err.Error(), "stream error:") || // hacky check for http2 errors
(errors.As(err, &httpErr) && retryafter.Should(httpErr.StatusCode, true))
}
func (cli *Client) downloadPossiblyEncryptedMediaWithRetries(url string, checksum []byte) (file, mac []byte, err error) {
for retryNum := 0; retryNum < 5; retryNum++ {
file, mac, err = cli.downloadEncryptedMedia(url, checksum)
if err == nil {
if checksum == nil {
file, err = cli.downloadMedia(url)
} else {
file, mac, err = cli.downloadEncryptedMedia(url, checksum)
}
if err == nil || !shouldRetryMediaDownload(err) {
return
}
netErr, ok := err.(net.Error)
if !ok {
// Not a network error, don't retry
return
retryDuration := time.Duration(retryNum+1) * time.Second
var httpErr DownloadHTTPError
if errors.As(err, &httpErr) {
retryDuration = retryafter.Parse(httpErr.Response.Header.Get("Retry-After"), retryDuration)
}
cli.Log.Warnf("Failed to download media due to network error: %w, retrying...", netErr)
time.Sleep(time.Duration(retryNum+1) * time.Second)
cli.Log.Warnf("Failed to download media due to network error: %w, retrying in %s...", err, retryDuration)
time.Sleep(retryDuration)
}
return
}
func (cli *Client) downloadEncryptedMedia(url string, checksum []byte) (file, mac []byte, err error) {
var req *http.Request
req, err = http.NewRequest(http.MethodGet, url, nil)
func (cli *Client) downloadMedia(url string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
err = fmt.Errorf("failed to prepare request: %w", err)
return
return nil, fmt.Errorf("failed to prepare request: %w", err)
}
req.Header.Set("Origin", socket.Origin)
req.Header.Set("Referer", socket.Origin+"/")
var resp *http.Response
resp, err = cli.http.Do(req)
if cli.MessengerConfig != nil {
req.Header.Set("User-Agent", cli.MessengerConfig.UserAgent)
}
// TODO user agent for whatsapp downloads?
resp, err := cli.http.Do(req)
if err != nil {
return
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusForbidden {
err = ErrMediaDownloadFailedWith403
} else if resp.StatusCode == http.StatusNotFound {
err = ErrMediaDownloadFailedWith404
} else if resp.StatusCode == http.StatusGone {
err = ErrMediaDownloadFailedWith410
} else {
err = fmt.Errorf("download failed with status code %d", resp.StatusCode)
}
return
return nil, DownloadHTTPError{Response: resp}
}
var data []byte
data, err = io.ReadAll(resp.Body)
return io.ReadAll(resp.Body)
}
func (cli *Client) downloadEncryptedMedia(url string, checksum []byte) (file, mac []byte, err error) {
data, err := cli.downloadMedia(url)
if err != nil {
return
} else if len(data) <= 10 {

View File

@ -9,6 +9,7 @@ package whatsmeow
import (
"errors"
"fmt"
"net/http"
waBinary "go.mau.fi/whatsmeow/binary"
)
@ -103,15 +104,28 @@ var (
var (
ErrBroadcastListUnsupported = errors.New("sending to non-status broadcast lists is not yet supported")
ErrUnknownServer = errors.New("can't send message to unknown server")
ErrRecipientADJID = errors.New("message recipient must be normal (non-AD) JID")
ErrRecipientADJID = errors.New("message recipient must be a user JID with no device part")
ErrServerReturnedError = errors.New("server returned error")
)
type DownloadHTTPError struct {
*http.Response
}
func (dhe DownloadHTTPError) Error() string {
return fmt.Sprintf("download failed with status code %d", dhe.StatusCode)
}
func (dhe DownloadHTTPError) Is(other error) bool {
var otherDHE DownloadHTTPError
return errors.As(other, &otherDHE) && dhe.StatusCode == otherDHE.StatusCode
}
// Some errors that Client.Download can return
var (
ErrMediaDownloadFailedWith403 = errors.New("download failed with status code 403")
ErrMediaDownloadFailedWith404 = errors.New("download failed with status code 404")
ErrMediaDownloadFailedWith410 = errors.New("download failed with status code 410")
ErrMediaDownloadFailedWith403 = DownloadHTTPError{Response: &http.Response{StatusCode: 403}}
ErrMediaDownloadFailedWith404 = DownloadHTTPError{Response: &http.Response{StatusCode: 404}}
ErrMediaDownloadFailedWith410 = DownloadHTTPError{Response: &http.Response{StatusCode: 410}}
ErrNoURLPresent = errors.New("no url present")
ErrFileLengthMismatch = errors.New("file length does not match")
ErrTooShortFile = errors.New("file too short")

21
vendor/go.mau.fi/whatsmeow/go.mod vendored Normal file
View File

@ -0,0 +1,21 @@
module go.mau.fi/whatsmeow
go 1.21
require (
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.0
github.com/rs/zerolog v1.32.0
go.mau.fi/libsignal v0.1.0
go.mau.fi/util v0.4.1
golang.org/x/crypto v0.23.0
golang.org/x/net v0.25.0
google.golang.org/protobuf v1.33.0
)
require (
filippo.io/edwards25519 v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
golang.org/x/sys v0.20.0 // indirect
)

44
vendor/go.mau.fi/whatsmeow/go.sum vendored Normal file
View File

@ -0,0 +1,44 @@
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c=
go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I=
go.mau.fi/util v0.4.1 h1:3EC9KxIXo5+h869zDGf5OOZklRd/FjeVnimTwtm3owg=
go.mau.fi/util v0.4.1/go.mod h1:GjkTEBsehYZbSh2LlE6cWEn+6ZIZTGrTMM/5DMNlmFY=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -148,30 +148,93 @@ const (
)
// UpdateGroupParticipants can be used to add, remove, promote and demote members in a WhatsApp group.
func (cli *Client) UpdateGroupParticipants(jid types.JID, participantChanges map[types.JID]ParticipantChange) (*waBinary.Node, error) {
func (cli *Client) UpdateGroupParticipants(jid types.JID, participantChanges []types.JID, action ParticipantChange) ([]types.GroupParticipant, error) {
content := make([]waBinary.Node, len(participantChanges))
i := 0
for participantJID, change := range participantChanges {
for i, participantJID := range participantChanges {
content[i] = waBinary.Node{
Tag: string(change),
Content: []waBinary.Node{{
Tag: "participant",
Attrs: waBinary.Attrs{"jid": participantJID},
}},
Tag: "participant",
Attrs: waBinary.Attrs{"jid": participantJID},
}
i++
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:g2",
Type: iqSet,
To: jid,
Content: content,
resp, err := cli.sendGroupIQ(context.TODO(), iqSet, jid, waBinary.Node{
Tag: string(action),
Content: content,
})
if err != nil {
return nil, err
}
// TODO proper return value?
return resp, nil
requestAction, ok := resp.GetOptionalChildByTag(string(action))
if !ok {
return nil, &ElementMissingError{Tag: string(action), In: "response to group participants update"}
}
requestParticipants := requestAction.GetChildrenByTag("participant")
participants := make([]types.GroupParticipant, len(requestParticipants))
for i, child := range requestParticipants {
participants[i] = parseParticipant(child.AttrGetter(), &child)
}
return participants, nil
}
// GetGroupRequestParticipants gets the list of participants that have requested to join the group.
func (cli *Client) GetGroupRequestParticipants(jid types.JID) ([]types.JID, error) {
resp, err := cli.sendGroupIQ(context.TODO(), iqGet, jid, waBinary.Node{
Tag: "membership_approval_requests",
})
if err != nil {
return nil, err
}
request, ok := resp.GetOptionalChildByTag("membership_approval_requests")
if !ok {
return nil, &ElementMissingError{Tag: "membership_approval_requests", In: "response to group request participants query"}
}
requestParticipants := request.GetChildrenByTag("membership_approval_request")
participants := make([]types.JID, len(requestParticipants))
for i, req := range requestParticipants {
participants[i] = req.AttrGetter().JID("jid")
}
return participants, nil
}
type ParticipantRequestChange string
const (
ParticipantChangeApprove ParticipantRequestChange = "approve"
ParticipantChangeReject ParticipantRequestChange = "reject"
)
// UpdateGroupRequestParticipants can be used to approve or reject requests to join the group.
func (cli *Client) UpdateGroupRequestParticipants(jid types.JID, participantChanges []types.JID, action ParticipantRequestChange) ([]types.GroupParticipant, error) {
content := make([]waBinary.Node, len(participantChanges))
for i, participantJID := range participantChanges {
content[i] = waBinary.Node{
Tag: "participant",
Attrs: waBinary.Attrs{"jid": participantJID},
}
}
resp, err := cli.sendGroupIQ(context.TODO(), iqSet, jid, waBinary.Node{
Tag: "membership_requests_action",
Content: []waBinary.Node{{
Tag: string(action),
Content: content,
}},
})
if err != nil {
return nil, err
}
request, ok := resp.GetOptionalChildByTag("membership_requests_action")
if !ok {
return nil, &ElementMissingError{Tag: "membership_requests_action", In: "response to group request participants update"}
}
requestAction, ok := request.GetOptionalChildByTag(string(action))
if !ok {
return nil, &ElementMissingError{Tag: string(action), In: "response to group request participants update"}
}
requestParticipants := requestAction.GetChildrenByTag("participant")
participants := make([]types.GroupParticipant, len(requestParticipants))
for i, child := range requestParticipants {
participants[i] = parseParticipant(child.AttrGetter(), &child)
}
return participants, nil
}
// SetGroupPhoto updates the group picture/icon of the given group on WhatsApp.
@ -501,6 +564,33 @@ func (cli *Client) getGroupMembers(ctx context.Context, jid types.JID) ([]types.
return cli.groupParticipantsCache[jid], nil
}
func parseParticipant(childAG *waBinary.AttrUtility, child *waBinary.Node) types.GroupParticipant {
pcpType := childAG.OptionalString("type")
participant := types.GroupParticipant{
IsAdmin: pcpType == "admin" || pcpType == "superadmin",
IsSuperAdmin: pcpType == "superadmin",
JID: childAG.JID("jid"),
LID: childAG.OptionalJIDOrEmpty("lid"),
DisplayName: childAG.OptionalString("display_name"),
}
if participant.JID.Server == types.HiddenUserServer && participant.LID.IsEmpty() {
participant.LID = participant.JID
//participant.JID = types.EmptyJID
}
if errorCode := childAG.OptionalInt("error"); errorCode != 0 {
participant.Error = errorCode
addRequest, ok := child.GetOptionalChildByTag("add_request")
if ok {
addAG := addRequest.AttrGetter()
participant.AddRequest = &types.GroupParticipantAddRequest{
Code: addAG.String("code"),
Expiration: addAG.UnixTime("expiration"),
}
}
}
return participant
}
func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, error) {
var group types.GroupInfo
ag := groupNode.AttrGetter()
@ -521,24 +611,7 @@ func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, e
childAG := child.AttrGetter()
switch child.Tag {
case "participant":
pcpType := childAG.OptionalString("type")
participant := types.GroupParticipant{
IsAdmin: pcpType == "admin" || pcpType == "superadmin",
IsSuperAdmin: pcpType == "superadmin",
JID: childAG.JID("jid"),
}
if errorCode := childAG.OptionalInt("error"); errorCode != 0 {
participant.Error = errorCode
addRequest, ok := child.GetOptionalChildByTag("add_request")
if ok {
addAG := addRequest.AttrGetter()
participant.AddRequest = &types.GroupParticipantAddRequest{
Code: addAG.String("code"),
Expiration: addAG.UnixTime("expiration"),
}
}
}
group.Participants = append(group.Participants, participant)
group.Participants = append(group.Participants, parseParticipant(childAG, &child))
case "description":
body, bodyOK := child.GetOptionalChildByTag("body")
if bodyOK {
@ -565,6 +638,9 @@ func (cli *Client) parseGroupNode(groupNode *waBinary.Node) (*types.GroupInfo, e
case "parent":
group.IsParent = true
group.DefaultMembershipApprovalMode = childAG.OptionalString("default_membership_approval_mode")
case "incognito":
group.IsIncognito = true
// TODO: membership_approval_mode
default:
cli.Log.Debugf("Unknown element in group node %s: %s", group.JID.String(), child.XMLString())
}

View File

@ -11,6 +11,7 @@ import (
"fmt"
"time"
"go.mau.fi/libsignal/ecc"
"google.golang.org/protobuf/proto"
waProto "go.mau.fi/whatsmeow/binary/proto"
@ -19,6 +20,9 @@ import (
)
const NoiseHandshakeResponseTimeout = 20 * time.Second
const WACertIssuerSerial = 0
var WACertPubKey = [...]byte{0x14, 0x23, 0x75, 0x57, 0x4d, 0xa, 0x58, 0x71, 0x66, 0xaa, 0xe7, 0x1e, 0xbe, 0x51, 0x64, 0x37, 0xc4, 0xa2, 0x8b, 0x73, 0xe3, 0x69, 0x5c, 0x6c, 0xe1, 0xf7, 0xf9, 0x54, 0x5d, 0xa8, 0xee, 0x6b}
// doHandshake implements the Noise_XX_25519_AESGCM_SHA256 handshake for the WhatsApp web API.
func (cli *Client) doHandshake(fs *socket.FrameSocket, ephemeralKP keys.KeyPair) error {
@ -76,23 +80,8 @@ func (cli *Client) doHandshake(fs *socket.FrameSocket, ephemeralKP keys.KeyPair)
certDecrypted, err := nh.Decrypt(certificateCiphertext)
if err != nil {
return fmt.Errorf("failed to decrypt noise certificate ciphertext: %w", err)
}
var cert waProto.NoiseCertificate
err = proto.Unmarshal(certDecrypted, &cert)
if err != nil {
return fmt.Errorf("failed to unmarshal noise certificate: %w", err)
}
certDetailsRaw := cert.GetDetails()
certSignature := cert.GetSignature()
if certDetailsRaw == nil || certSignature == nil {
return fmt.Errorf("missing parts of noise certificate")
}
var certDetails waProto.NoiseCertificate_Details
err = proto.Unmarshal(certDetailsRaw, &certDetails)
if err != nil {
return fmt.Errorf("failed to unmarshal noise certificate details: %w", err)
} else if !bytes.Equal(certDetails.GetKey(), staticDecrypted) {
return fmt.Errorf("cert key doesn't match decrypted static")
} else if err = verifyServerCert(certDecrypted, staticDecrypted); err != nil {
return fmt.Errorf("failed to verify server cert: %w", err)
}
encryptedPubkey := nh.Encrypt(cli.Store.NoiseKey.Pub[:])
@ -101,7 +90,14 @@ func (cli *Client) doHandshake(fs *socket.FrameSocket, ephemeralKP keys.KeyPair)
return fmt.Errorf("failed to mix noise private key in: %w", err)
}
clientFinishPayloadBytes, err := proto.Marshal(cli.Store.GetClientPayload())
var clientPayload *waProto.ClientPayload
if cli.GetClientPayload != nil {
clientPayload = cli.GetClientPayload()
} else {
clientPayload = cli.Store.GetClientPayload()
}
clientFinishPayloadBytes, err := proto.Marshal(clientPayload)
if err != nil {
return fmt.Errorf("failed to marshal client finish payload: %w", err)
}
@ -129,3 +125,40 @@ func (cli *Client) doHandshake(fs *socket.FrameSocket, ephemeralKP keys.KeyPair)
return nil
}
func verifyServerCert(certDecrypted, staticDecrypted []byte) error {
var certChain waProto.CertChain
err := proto.Unmarshal(certDecrypted, &certChain)
if err != nil {
return fmt.Errorf("failed to unmarshal noise certificate: %w", err)
}
var intermediateCertDetails, leafCertDetails waProto.CertChain_NoiseCertificate_Details
intermediateCertDetailsRaw := certChain.GetIntermediate().GetDetails()
intermediateCertSignature := certChain.GetIntermediate().GetSignature()
leafCertDetailsRaw := certChain.GetLeaf().GetDetails()
leafCertSignature := certChain.GetLeaf().GetSignature()
if intermediateCertDetailsRaw == nil || intermediateCertSignature == nil || leafCertDetailsRaw == nil || leafCertSignature == nil {
return fmt.Errorf("missing parts of noise certificate")
} else if len(intermediateCertSignature) != 64 {
return fmt.Errorf("unexpected length of intermediate cert signature %d (expected 64)", len(intermediateCertSignature))
} else if len(leafCertSignature) != 64 {
return fmt.Errorf("unexpected length of leaf cert signature %d (expected 64)", len(leafCertSignature))
} else if !ecc.VerifySignature(ecc.NewDjbECPublicKey(WACertPubKey), intermediateCertDetailsRaw, [64]byte(intermediateCertSignature)) {
return fmt.Errorf("failed to verify intermediate cert signature")
} else if err = proto.Unmarshal(intermediateCertDetailsRaw, &intermediateCertDetails); err != nil {
return fmt.Errorf("failed to unmarshal noise certificate details: %w", err)
} else if intermediateCertDetails.GetIssuerSerial() != WACertIssuerSerial {
return fmt.Errorf("unexpected intermediate issuer serial %d (expected %d)", intermediateCertDetails.GetIssuerSerial(), WACertIssuerSerial)
} else if len(intermediateCertDetails.GetKey()) != 32 {
return fmt.Errorf("unexpected length of intermediate cert key %d (expected 32)", len(intermediateCertDetails.GetKey()))
} else if !ecc.VerifySignature(ecc.NewDjbECPublicKey([32]byte(intermediateCertDetails.GetKey())), leafCertDetailsRaw, [64]byte(leafCertSignature)) {
return fmt.Errorf("failed to verify intermediate cert signature")
} else if err = proto.Unmarshal(leafCertDetailsRaw, &leafCertDetails); err != nil {
return fmt.Errorf("failed to unmarshal noise certificate details: %w", err)
} else if leafCertDetails.GetIssuerSerial() != intermediateCertDetails.GetSerial() {
return fmt.Errorf("unexpected leaf issuer serial %d (expected %d)", leafCertDetails.GetIssuerSerial(), intermediateCertDetails.GetSerial())
} else if !bytes.Equal(leafCertDetails.GetKey(), staticDecrypted) {
return fmt.Errorf("cert key doesn't match decrypted static")
}
return nil
}

View File

@ -9,6 +9,8 @@ package whatsmeow
import (
"context"
"go.mau.fi/libsignal/keys/prekey"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
)
@ -66,3 +68,19 @@ func (int *DangerousInternalClient) RequestAppStateKeys(ctx context.Context, key
func (int *DangerousInternalClient) SendRetryReceipt(node *waBinary.Node, info *types.MessageInfo, forceIncludeIdentity bool) {
int.c.sendRetryReceipt(node, info, forceIncludeIdentity)
}
func (int *DangerousInternalClient) EncryptMessageForDevice(plaintext []byte, to types.JID, bundle *prekey.Bundle, extraAttrs waBinary.Attrs) (*waBinary.Node, bool, error) {
return int.c.encryptMessageForDevice(plaintext, to, bundle, extraAttrs)
}
func (int *DangerousInternalClient) GetOwnID() types.JID {
return int.c.getOwnID()
}
func (int *DangerousInternalClient) DecryptDM(child *waBinary.Node, from types.JID, isPreKey bool) ([]byte, error) {
return int.c.decryptDM(child, from, isPreKey)
}
func (int *DangerousInternalClient) MakeDeviceIdentityNode() waBinary.Node {
return int.c.makeDeviceIdentityNode()
}

View File

@ -11,7 +11,6 @@ import (
"math/rand"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
@ -67,7 +66,6 @@ func (cli *Client) sendKeepAlive(ctx context.Context) (isSuccess, shouldContinue
Namespace: "w:p",
Type: "get",
To: types.ServerJID,
Content: []waBinary.Node{{Tag: "ping"}},
})
if err != nil {
cli.Log.Warnf("Failed to send keepalive: %v", err)

View File

@ -0,0 +1,11 @@
# mdtest
This is a simple tool for testing whatsmeow.
1. Clone the repository.
2. Run `go build` inside this directory.
3. Run `./mdtest` to start the program. Optionally, use `rlwrap ./mdtest` to get a nicer prompt.
Add `-debug` if you want to see the raw data being sent/received.
4. On the first run, scan the QR code. On future runs, the program will remember you (unless `mdtest.db` is deleted).
New messages will be automatically logged. To send a message, use `send <jid> <message>`

View File

@ -0,0 +1,29 @@
module go.mau.fi/whatsmeow/mdtest
go 1.21
toolchain go1.22.0
require (
github.com/mattn/go-sqlite3 v1.14.22
github.com/mdp/qrterminal/v3 v3.0.0
go.mau.fi/whatsmeow v0.0.0-20230805111647-405414b9b5c0
google.golang.org/protobuf v1.33.0
)
require (
filippo.io/edwards25519 v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/rs/zerolog v1.32.0 // indirect
go.mau.fi/libsignal v0.1.0 // indirect
go.mau.fi/util v0.4.1 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
rsc.io/qr v0.2.0 // indirect
)
replace go.mau.fi/whatsmeow => ../

View File

@ -0,0 +1,54 @@
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ=
github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ=
github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXlBqXQEwvE0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c=
go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I=
go.mau.fi/util v0.4.1 h1:3EC9KxIXo5+h869zDGf5OOZklRd/FjeVnimTwtm3owg=
go.mau.fi/util v0.4.1/go.mod h1:GjkTEBsehYZbSh2LlE6cWEn+6ZIZTGrTMM/5DMNlmFY=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=

1165
vendor/go.mau.fi/whatsmeow/mdtest/main.go vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ package whatsmeow
import (
"fmt"
"go.mau.fi/util/random"
"google.golang.org/protobuf/proto"
waBinary "go.mau.fi/whatsmeow/binary"
@ -17,7 +18,6 @@ import (
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/gcmutil"
"go.mau.fi/whatsmeow/util/hkdfutil"
"go.mau.fi/whatsmeow/util/randbytes"
)
func getMediaRetryKey(mediaKey []byte) (cipherKey []byte) {
@ -34,7 +34,7 @@ func encryptMediaRetryReceipt(messageID types.MessageID, mediaKey []byte) (ciphe
err = fmt.Errorf("failed to marshal payload: %w", err)
return
}
iv = randbytes.Make(12)
iv = random.Bytes(12)
ciphertext, err = gcmutil.Encrypt(getMediaRetryKey(mediaKey), iv, plaintext, []byte(messageID))
return
}

View File

@ -14,15 +14,14 @@ import (
"fmt"
"io"
"runtime/debug"
"sync/atomic"
"time"
"go.mau.fi/libsignal/signalerror"
"google.golang.org/protobuf/proto"
"go.mau.fi/libsignal/groups"
"go.mau.fi/libsignal/protocol"
"go.mau.fi/libsignal/session"
"go.mau.fi/libsignal/signalerror"
"go.mau.fi/util/random"
"google.golang.org/protobuf/proto"
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
@ -30,7 +29,6 @@ import (
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/randbytes"
)
var pbSerializer = store.SignalProtobufSerializer
@ -46,7 +44,12 @@ func (cli *Client) handleEncryptedMessage(node *waBinary.Node) {
if len(info.PushName) > 0 && info.PushName != "-" {
go cli.updatePushName(info.Sender, info, info.PushName)
}
cli.decryptMessages(info, node)
go cli.sendAck(node)
if info.Sender.Server == types.NewsletterServer {
cli.handlePlaintextMessage(info, node)
} else {
cli.decryptMessages(info, node)
}
}
}
@ -72,6 +75,10 @@ func (cli *Client) parseMessageSource(node *waBinary.Node, requireParticipant bo
if from.Server == types.BroadcastServer {
source.BroadcastListOwner = ag.OptionalJIDOrEmpty("recipient")
}
} else if from.Server == types.NewsletterServer {
source.Chat = from
source.Sender = from
// TODO IsFromMe?
} else if from.User == clientID.User {
source.IsFromMe = true
source.Sender = from
@ -98,40 +105,83 @@ func (cli *Client) parseMessageInfo(node *waBinary.Node) (*types.MessageInfo, er
}
ag := node.AttrGetter()
info.ID = types.MessageID(ag.String("id"))
info.ServerID = types.MessageServerID(ag.OptionalInt("server_id"))
info.Timestamp = ag.UnixTime("t")
info.PushName = ag.OptionalString("notify")
info.Category = ag.OptionalString("category")
info.Type = ag.OptionalString("type")
info.Edit = types.EditAttribute(ag.OptionalString("edit"))
if !ag.OK() {
return nil, ag.Error()
}
for _, child := range node.GetChildren() {
if child.Tag == "multicast" {
switch child.Tag {
case "multicast":
info.Multicast = true
} else if child.Tag == "verified_name" {
case "verified_name":
info.VerifiedName, err = parseVerifiedNameContent(child)
if err != nil {
cli.Log.Warnf("Failed to parse verified_name node in %s: %v", info.ID, err)
}
} else if mediaType, ok := child.AttrGetter().GetString("mediatype", false); ok {
info.MediaType = mediaType
case "franking":
// TODO
case "trace":
// TODO
default:
if mediaType, ok := child.AttrGetter().GetString("mediatype", false); ok {
info.MediaType = mediaType
}
}
}
return &info, nil
}
func (cli *Client) handlePlaintextMessage(info *types.MessageInfo, node *waBinary.Node) {
// TODO edits have an additional <meta msg_edit_t="1696321271735" original_msg_t="1696321248"/> node
plaintext, ok := node.GetOptionalChildByTag("plaintext")
if !ok {
// 3:
return
}
plaintextBody, ok := plaintext.Content.([]byte)
if !ok {
cli.Log.Warnf("Plaintext message from %s doesn't have byte content", info.SourceString())
return
}
var msg waProto.Message
err := proto.Unmarshal(plaintextBody, &msg)
if err != nil {
cli.Log.Warnf("Error unmarshaling plaintext message from %s: %v", info.SourceString(), err)
return
}
cli.storeMessageSecret(info, &msg)
evt := &events.Message{
Info: *info,
RawMessage: &msg,
}
meta, ok := node.GetOptionalChildByTag("meta")
if ok {
evt.NewsletterMeta = &events.NewsletterMessageMeta{
EditTS: meta.AttrGetter().UnixMilli("msg_edit_t"),
OriginalTS: meta.AttrGetter().UnixTime("original_msg_t"),
}
}
cli.dispatchEvent(evt.UnwrapRaw())
return
}
func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node) {
go cli.sendAck(node)
if len(node.GetChildrenByTag("unavailable")) > 0 && len(node.GetChildrenByTag("enc")) == 0 {
cli.Log.Warnf("Unavailable message %s from %s", info.ID, info.SourceString())
go cli.sendRetryReceipt(node, info, true)
cli.dispatchEvent(&events.UndecryptableMessage{Info: *info, IsUnavailable: true})
return
}
children := node.GetChildren()
cli.Log.Debugf("Decrypting %d messages from %s", len(children), info.SourceString())
cli.Log.Debugf("Decrypting message from %s", info.SourceString())
handled := false
containsDirectMsg := false
for _, child := range children {
@ -158,28 +208,33 @@ func (cli *Client) decryptMessages(info *types.MessageInfo, node *waBinary.Node)
cli.Log.Warnf("Error decrypting message from %s: %v", info.SourceString(), err)
isUnavailable := encType == "skmsg" && !containsDirectMsg && errors.Is(err, signalerror.ErrNoSenderKeyForUser)
go cli.sendRetryReceipt(node, info, isUnavailable)
decryptFailMode, _ := child.Attrs["decrypt-fail"].(string)
cli.dispatchEvent(&events.UndecryptableMessage{
Info: *info,
IsUnavailable: isUnavailable,
DecryptFailMode: events.DecryptFailMode(decryptFailMode),
DecryptFailMode: events.DecryptFailMode(ag.OptionalString("decrypt-fail")),
})
return
}
var msg waProto.Message
err = proto.Unmarshal(decrypted, &msg)
if err != nil {
cli.Log.Warnf("Error unmarshaling decrypted message from %s: %v", info.SourceString(), err)
continue
}
retryCount := ag.OptionalInt("count")
if retryCount > 0 {
cli.cancelDelayedRequestFromPhone(info.ID)
}
cli.handleDecryptedMessage(info, &msg, retryCount)
handled = true
var msg waProto.Message
switch ag.Int("v") {
case 2:
err = proto.Unmarshal(decrypted, &msg)
if err != nil {
cli.Log.Warnf("Error unmarshaling decrypted message from %s: %v", info.SourceString(), err)
continue
}
cli.handleDecryptedMessage(info, &msg, retryCount)
handled = true
case 3:
handled = cli.handleDecryptedArmadillo(info, decrypted, retryCount)
default:
cli.Log.Warnf("Unknown version %d in decrypted message from %s", ag.Int("v"), info.SourceString())
}
}
if handled {
go cli.sendMessageReceipt(info)
@ -228,6 +283,9 @@ func (cli *Client) decryptDM(child *waBinary.Node, from types.JID, isPreKey bool
return nil, fmt.Errorf("failed to decrypt normal message: %w", err)
}
}
if child.AttrGetter().Int("v") == 3 {
return plaintext, nil
}
return unpadMessage(plaintext)
}
@ -245,6 +303,9 @@ func (cli *Client) decryptGroupMsg(child *waBinary.Node, from types.JID, chat ty
if err != nil {
return nil, fmt.Errorf("failed to decrypt group message: %w", err)
}
if child.AttrGetter().Int("v") == 3 {
return plaintext, nil
}
return unpadMessage(plaintext)
}
@ -267,19 +328,19 @@ func unpadMessage(plaintext []byte) ([]byte, error) {
}
func padMessage(plaintext []byte) []byte {
pad := randbytes.Make(1)
pad := random.Bytes(1)
pad[0] &= 0xf
if pad[0] == 0 {
pad[0] = 0xf
}
plaintext = append(plaintext, bytes.Repeat(pad[:], int(pad[0]))...)
plaintext = append(plaintext, bytes.Repeat(pad, int(pad[0]))...)
return plaintext
}
func (cli *Client) handleSenderKeyDistributionMessage(chat, from types.JID, rawSKDMsg *waProto.SenderKeyDistributionMessage) {
func (cli *Client) handleSenderKeyDistributionMessage(chat, from types.JID, axolotlSKDM []byte) {
builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer)
senderKeyName := protocol.NewSenderKeyName(chat.String(), from.SignalAddress())
sdkMsg, err := protocol.NewSenderKeyDistributionMessageFromBytes(rawSKDMsg.AxolotlSenderKeyDistributionMessage, pbSerializer.SenderKeyDistributionMessage)
sdkMsg, err := protocol.NewSenderKeyDistributionMessageFromBytes(axolotlSKDM, pbSerializer.SenderKeyDistributionMessage)
if err != nil {
cli.Log.Errorf("Failed to parse sender key distribution message from %s for %s: %v", from, chat, err)
return
@ -290,7 +351,7 @@ func (cli *Client) handleSenderKeyDistributionMessage(chat, from types.JID, rawS
func (cli *Client) handleHistorySyncNotificationLoop() {
defer func() {
atomic.StoreUint32(&cli.historySyncHandlerStarted, 0)
cli.historySyncHandlerStarted.Store(false)
err := recover()
if err != nil {
cli.Log.Errorf("History sync handler panicked: %v\n%s", err, debug.Stack())
@ -298,7 +359,7 @@ func (cli *Client) handleHistorySyncNotificationLoop() {
// Check in case something new appeared in the channel between the loop stopping
// and the atomic variable being updated. If yes, restart the loop.
if len(cli.historySyncNotifications) > 0 && atomic.CompareAndSwapUint32(&cli.historySyncHandlerStarted, 0, 1) {
if len(cli.historySyncNotifications) > 0 && cli.historySyncHandlerStarted.CompareAndSwap(false, true) {
cli.Log.Warnf("New history sync notifications appeared after loop stopped, restarting loop...")
go cli.handleHistorySyncNotificationLoop()
}
@ -391,10 +452,10 @@ func (cli *Client) handleProtocolMessage(info *types.MessageInfo, msg *waProto.M
if protoMsg.GetHistorySyncNotification() != nil && info.IsFromMe {
cli.historySyncNotifications <- protoMsg.HistorySyncNotification
if atomic.CompareAndSwapUint32(&cli.historySyncHandlerStarted, 0, 1) {
if cli.historySyncHandlerStarted.CompareAndSwap(false, true) {
go cli.handleHistorySyncNotificationLoop()
}
go cli.sendProtocolMessageReceipt(info.ID, "hist_sync")
go cli.sendProtocolMessageReceipt(info.ID, types.ReceiptTypeHistorySync)
}
if protoMsg.GetPeerDataOperationRequestResponseMessage().GetPeerDataOperationRequestType() == waProto.PeerDataOperationRequestType_PLACEHOLDER_MESSAGE_RESEND {
@ -406,7 +467,7 @@ func (cli *Client) handleProtocolMessage(info *types.MessageInfo, msg *waProto.M
}
if info.Category == "peer" {
go cli.sendProtocolMessageReceipt(info.ID, "peer_msg")
go cli.sendProtocolMessageReceipt(info.ID, types.ReceiptTypePeerMsg)
}
}
@ -417,9 +478,9 @@ func (cli *Client) processProtocolParts(info *types.MessageInfo, msg *waProto.Me
}
if msg.GetSenderKeyDistributionMessage() != nil {
if !info.IsGroup {
cli.Log.Warnf("Got sender key distribution message in non-group chat from", info.Sender)
cli.Log.Warnf("Got sender key distribution message in non-group chat from %s", info.Sender)
} else {
cli.handleSenderKeyDistributionMessage(info.Chat, info.Sender, msg.SenderKeyDistributionMessage)
cli.handleSenderKeyDistributionMessage(info.Chat, info.Sender, msg.SenderKeyDistributionMessage.AxolotlSenderKeyDistributionMessage)
}
}
// N.B. Edits are protocol messages, but they're also wrapped inside EditedMessage,
@ -427,6 +488,10 @@ func (cli *Client) processProtocolParts(info *types.MessageInfo, msg *waProto.Me
if msg.GetProtocolMessage() != nil {
cli.handleProtocolMessage(info, msg)
}
cli.storeMessageSecret(info, msg)
}
func (cli *Client) storeMessageSecret(info *types.MessageInfo, msg *waProto.Message) {
if msgSecret := msg.GetMessageContextInfo().GetMessageSecret(); len(msgSecret) > 0 {
err := cli.Store.MsgSecrets.PutMessageSecret(info.Chat, info.Sender, info.ID, msgSecret)
if err != nil {
@ -511,7 +576,7 @@ func (cli *Client) handleDecryptedMessage(info *types.MessageInfo, msg *waProto.
cli.dispatchEvent(evt.UnwrapRaw())
}
func (cli *Client) sendProtocolMessageReceipt(id, msgType string) {
func (cli *Client) sendProtocolMessageReceipt(id types.MessageID, msgType types.ReceiptType) {
clientID := cli.Store.ID
if len(id) == 0 || clientID == nil {
return
@ -519,8 +584,8 @@ func (cli *Client) sendProtocolMessageReceipt(id, msgType string) {
err := cli.sendNode(waBinary.Node{
Tag: "receipt",
Attrs: waBinary.Attrs{
"id": id,
"type": msgType,
"id": string(id),
"type": string(msgType),
"to": types.NewJID(clientID.User, types.LegacyUserServer),
},
Content: nil,

View File

@ -11,6 +11,7 @@ import (
"fmt"
"time"
"go.mau.fi/util/random"
"google.golang.org/protobuf/proto"
waProto "go.mau.fi/whatsmeow/binary/proto"
@ -18,7 +19,6 @@ import (
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/gcmutil"
"go.mau.fi/whatsmeow/util/hkdfutil"
"go.mau.fi/whatsmeow/util/randbytes"
)
type MsgSecretType string
@ -107,7 +107,7 @@ func (cli *Client) encryptMsgSecret(chat, origSender types.JID, origMsgID types.
}
secretKey, additionalData := generateMsgSecretKey(useCase, ownID, origMsgID, origSender, baseEncKey)
iv = randbytes.Make(12)
iv = random.Bytes(12)
ciphertext, err = gcmutil.Encrypt(secretKey, iv, plaintext, additionalData)
if err != nil {
return nil, nil, fmt.Errorf("failed to encrypt secret message: %w", err)
@ -221,7 +221,7 @@ func (cli *Client) BuildPollVote(pollInfo *types.MessageInfo, optionNames []stri
//
// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildPollCreation("meow?", []string{"yes", "no"}, 1))
func (cli *Client) BuildPollCreation(name string, optionNames []string, selectableOptionCount int) *waProto.Message {
msgSecret := randbytes.Make(32)
msgSecret := random.Bytes(32)
if selectableOptionCount < 0 || selectableOptionCount > len(optionNames) {
selectableOptionCount = 0
}

373
vendor/go.mau.fi/whatsmeow/newsletter.go vendored Normal file
View File

@ -0,0 +1,373 @@
// Copyright (c) 2023 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
)
// NewsletterSubscribeLiveUpdates subscribes to receive live updates from a WhatsApp channel temporarily (for the duration returned).
func (cli *Client) NewsletterSubscribeLiveUpdates(ctx context.Context, jid types.JID) (time.Duration, error) {
resp, err := cli.sendIQ(infoQuery{
Context: ctx,
Namespace: "newsletter",
Type: iqSet,
To: jid,
Content: []waBinary.Node{{
Tag: "live_updates",
}},
})
if err != nil {
return 0, err
}
child := resp.GetChildByTag("live_updates")
dur := child.AttrGetter().Int("duration")
return time.Duration(dur) * time.Second, nil
}
// NewsletterMarkViewed marks a channel message as viewed, incrementing the view counter.
//
// This is not the same as marking the channel as read on your other devices, use the usual MarkRead function for that.
func (cli *Client) NewsletterMarkViewed(jid types.JID, serverIDs []types.MessageServerID) error {
items := make([]waBinary.Node, len(serverIDs))
for i, id := range serverIDs {
items[i] = waBinary.Node{
Tag: "item",
Attrs: waBinary.Attrs{
"server_id": id,
},
}
}
reqID := cli.generateRequestID()
resp := cli.waitResponse(reqID)
err := cli.sendNode(waBinary.Node{
Tag: "receipt",
Attrs: waBinary.Attrs{
"to": jid,
"type": "view",
"id": reqID,
},
Content: []waBinary.Node{{
Tag: "list",
Content: items,
}},
})
if err != nil {
cli.cancelResponse(reqID, resp)
return err
}
// TODO handle response?
<-resp
return nil
}
// NewsletterSendReaction sends a reaction to a channel message.
// To remove a reaction sent earlier, set reaction to an empty string.
//
// The last parameter is the message ID of the reaction itself. It can be left empty to let whatsmeow generate a random one.
func (cli *Client) NewsletterSendReaction(jid types.JID, serverID types.MessageServerID, reaction string, messageID types.MessageID) error {
if messageID == "" {
messageID = cli.GenerateMessageID()
}
reactionAttrs := waBinary.Attrs{}
messageAttrs := waBinary.Attrs{
"to": jid,
"id": messageID,
"server_id": serverID,
"type": "reaction",
}
if reaction != "" {
reactionAttrs["code"] = reaction
} else {
messageAttrs["edit"] = string(types.EditAttributeSenderRevoke)
}
return cli.sendNode(waBinary.Node{
Tag: "message",
Attrs: messageAttrs,
Content: []waBinary.Node{{
Tag: "reaction",
Attrs: reactionAttrs,
}},
})
}
const (
queryFetchNewsletter = "6563316087068696"
queryFetchNewsletterDehydrated = "7272540469429201"
queryRecommendedNewsletters = "7263823273662354" //variables -> input -> {limit: 20, country_codes: [string]}, output: xwa2_newsletters_recommended
queryNewslettersDirectory = "6190824427689257" // variables -> input -> {view: "RECOMMENDED", limit: 50, start_cursor: base64, filters: {country_codes: [string]}}
querySubscribedNewsletters = "6388546374527196" // variables -> empty, output: xwa2_newsletter_subscribed
queryNewsletterSubscribers = "9800646650009898" //variables -> input -> {newsletter_id, count}, output: xwa2_newsletter_subscribers -> subscribers -> edges
mutationMuteNewsletter = "6274038279359549" //variables -> {newsletter_id, updates->{description, settings}}, output: xwa2_newsletter_update -> NewsletterMetadata without viewer meta
mutationUnmuteNewsletter = "6068417879924485"
mutationUpdateNewsletter = "7150902998257522"
mutationCreateNewsletter = "6234210096708695"
mutationUnfollowNewsletter = "6392786840836363"
mutationFollowNewsletter = "9926858900719341"
)
func (cli *Client) sendMexIQ(ctx context.Context, queryID string, variables any) (json.RawMessage, error) {
payload, err := json.Marshal(map[string]any{
"variables": variables,
})
if err != nil {
return nil, err
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "w:mex",
Type: iqGet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "query",
Attrs: waBinary.Attrs{
"query_id": queryID,
},
Content: payload,
}},
Context: ctx,
})
if err != nil {
return nil, err
}
result, ok := resp.GetOptionalChildByTag("result")
if !ok {
return nil, &ElementMissingError{Tag: "result", In: "mex response"}
}
resultContent, ok := result.Content.([]byte)
if !ok {
return nil, fmt.Errorf("unexpected content type %T in mex response", result.Content)
}
var gqlResp types.GraphQLResponse
err = json.Unmarshal(resultContent, &gqlResp)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal graphql response: %w", err)
} else if len(gqlResp.Errors) > 0 {
return gqlResp.Data, fmt.Errorf("graphql error: %w", gqlResp.Errors)
}
return gqlResp.Data, nil
}
type respGetNewsletterInfo struct {
Newsletter *types.NewsletterMetadata `json:"xwa2_newsletter"`
}
func (cli *Client) getNewsletterInfo(input map[string]any, fetchViewerMeta bool) (*types.NewsletterMetadata, error) {
data, err := cli.sendMexIQ(context.TODO(), queryFetchNewsletter, map[string]any{
"fetch_creation_time": true,
"fetch_full_image": true,
"fetch_viewer_metadata": fetchViewerMeta,
"input": input,
})
var respData respGetNewsletterInfo
if data != nil {
jsonErr := json.Unmarshal(data, &respData)
if err == nil && jsonErr != nil {
err = jsonErr
}
}
return respData.Newsletter, err
}
// GetNewsletterInfo gets the info of a newsletter that you're joined to.
func (cli *Client) GetNewsletterInfo(jid types.JID) (*types.NewsletterMetadata, error) {
return cli.getNewsletterInfo(map[string]any{
"key": jid.String(),
"type": types.NewsletterKeyTypeJID,
}, true)
}
// GetNewsletterInfoWithInvite gets the info of a newsletter with an invite link.
//
// You can either pass the full link (https://whatsapp.com/channel/...) or just the `...` part.
//
// Note that the ViewerMeta field of the returned NewsletterMetadata will be nil.
func (cli *Client) GetNewsletterInfoWithInvite(key string) (*types.NewsletterMetadata, error) {
return cli.getNewsletterInfo(map[string]any{
"key": strings.TrimPrefix(key, NewsletterLinkPrefix),
"type": types.NewsletterKeyTypeInvite,
}, false)
}
type respGetSubscribedNewsletters struct {
Newsletters []*types.NewsletterMetadata `json:"xwa2_newsletter_subscribed"`
}
// GetSubscribedNewsletters gets the info of all newsletters that you're joined to.
func (cli *Client) GetSubscribedNewsletters() ([]*types.NewsletterMetadata, error) {
data, err := cli.sendMexIQ(context.TODO(), querySubscribedNewsletters, map[string]any{})
var respData respGetSubscribedNewsletters
if data != nil {
jsonErr := json.Unmarshal(data, &respData)
if err == nil && jsonErr != nil {
err = jsonErr
}
}
return respData.Newsletters, err
}
type CreateNewsletterParams struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Picture []byte `json:"picture,omitempty"`
}
type respCreateNewsletter struct {
Newsletter *types.NewsletterMetadata `json:"xwa2_newsletter_create"`
}
// CreateNewsletter creates a new WhatsApp channel.
func (cli *Client) CreateNewsletter(params CreateNewsletterParams) (*types.NewsletterMetadata, error) {
resp, err := cli.sendMexIQ(context.TODO(), mutationCreateNewsletter, map[string]any{
"newsletter_input": &params,
})
if err != nil {
return nil, err
}
var respData respCreateNewsletter
err = json.Unmarshal(resp, &respData)
if err != nil {
return nil, err
}
return respData.Newsletter, nil
}
// AcceptTOSNotice accepts a ToS notice.
//
// To accept the terms for creating newsletters, use
//
// cli.AcceptTOSNotice("20601218", "5")
func (cli *Client) AcceptTOSNotice(noticeID, stage string) error {
_, err := cli.sendIQ(infoQuery{
Namespace: "tos",
Type: iqSet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "notice",
Attrs: waBinary.Attrs{
"id": noticeID,
"stage": stage,
},
}},
})
return err
}
// NewsletterToggleMute changes the mute status of a newsletter.
func (cli *Client) NewsletterToggleMute(jid types.JID, mute bool) error {
query := mutationUnmuteNewsletter
if mute {
query = mutationMuteNewsletter
}
_, err := cli.sendMexIQ(context.TODO(), query, map[string]any{
"newsletter_id": jid.String(),
})
return err
}
// FollowNewsletter makes the user follow (join) a WhatsApp channel.
func (cli *Client) FollowNewsletter(jid types.JID) error {
_, err := cli.sendMexIQ(context.TODO(), mutationFollowNewsletter, map[string]any{
"newsletter_id": jid.String(),
})
return err
}
// UnfollowNewsletter makes the user unfollow (leave) a WhatsApp channel.
func (cli *Client) UnfollowNewsletter(jid types.JID) error {
_, err := cli.sendMexIQ(context.TODO(), mutationUnfollowNewsletter, map[string]any{
"newsletter_id": jid.String(),
})
return err
}
type GetNewsletterMessagesParams struct {
Count int
Before types.MessageServerID
}
// GetNewsletterMessages gets messages in a WhatsApp channel.
func (cli *Client) GetNewsletterMessages(jid types.JID, params *GetNewsletterMessagesParams) ([]*types.NewsletterMessage, error) {
attrs := waBinary.Attrs{
"type": "jid",
"jid": jid,
}
if params != nil {
if params.Count != 0 {
attrs["count"] = params.Count
}
if params.Before != 0 {
attrs["before"] = params.Before
}
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "newsletter",
Type: iqGet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "messages",
Attrs: attrs,
}},
Context: context.TODO(),
})
if err != nil {
return nil, err
}
messages, ok := resp.GetOptionalChildByTag("messages")
if !ok {
return nil, &ElementMissingError{Tag: "messages", In: "newsletter messages response"}
}
return cli.parseNewsletterMessages(&messages), nil
}
type GetNewsletterUpdatesParams struct {
Count int
Since time.Time
After types.MessageServerID
}
// GetNewsletterMessageUpdates gets updates in a WhatsApp channel.
//
// These are the same kind of updates that NewsletterSubscribeLiveUpdates triggers (reaction and view counts).
func (cli *Client) GetNewsletterMessageUpdates(jid types.JID, params *GetNewsletterUpdatesParams) ([]*types.NewsletterMessage, error) {
attrs := waBinary.Attrs{}
if params != nil {
if params.Count != 0 {
attrs["count"] = params.Count
}
if !params.Since.IsZero() {
attrs["since"] = params.Since.Unix()
}
if params.After != 0 {
attrs["after"] = params.After
}
}
resp, err := cli.sendIQ(infoQuery{
Namespace: "newsletter",
Type: iqGet,
To: jid,
Content: []waBinary.Node{{
Tag: "message_updates",
Attrs: attrs,
}},
Context: context.TODO(),
})
if err != nil {
return nil, err
}
messages, ok := resp.GetOptionalChildByTag("message_updates", "messages")
if !ok {
return nil, &ElementMissingError{Tag: "messages", In: "newsletter messages response"}
}
return cli.parseNewsletterMessages(&messages), nil
}

View File

@ -7,10 +7,14 @@
package whatsmeow
import (
"encoding/json"
"errors"
"google.golang.org/protobuf/proto"
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
@ -100,7 +104,7 @@ func (cli *Client) handleDeviceNotification(node *waBinary.Node) {
cli.Log.Debugf("No device list cached for %s, ignoring device list notification", from)
return
}
cachedParticipantHash := participantListHashV2(cached)
cachedParticipantHash := participantListHashV2(cached.devices)
for _, child := range node.GetChildren() {
if child.Tag != "add" && child.Tag != "remove" {
cli.Log.Debugf("Unknown device list change tag %s", child.Tag)
@ -112,17 +116,17 @@ func (cli *Client) handleDeviceNotification(node *waBinary.Node) {
changedDeviceJID := deviceChild.AttrGetter().JID("jid")
switch child.Tag {
case "add":
cached = append(cached, changedDeviceJID)
cached.devices = append(cached.devices, changedDeviceJID)
case "remove":
for i, jid := range cached {
for i, jid := range cached.devices {
if jid == changedDeviceJID {
cached = append(cached[:i], cached[i+1:]...)
cached.devices = append(cached.devices[:i], cached.devices[i+1:]...)
}
}
case "update":
// ???
}
newParticipantHash := participantListHashV2(cached)
newParticipantHash := participantListHashV2(cached.devices)
if newParticipantHash == deviceHash {
cli.Log.Debugf("%s's device list hash changed from %s to %s (%s). New hash matches", from, cachedParticipantHash, deviceHash, child.Tag)
cli.userDevicesCache[from] = cached
@ -133,6 +137,14 @@ func (cli *Client) handleDeviceNotification(node *waBinary.Node) {
}
}
func (cli *Client) handleFBDeviceNotification(node *waBinary.Node) {
cli.userDevicesCacheLock.Lock()
defer cli.userDevicesCacheLock.Unlock()
jid := node.AttrGetter().JID("from")
userDevices := parseFBDeviceList(jid, node.GetChildByTag("devices"))
cli.userDevicesCache[jid] = userDevices
}
func (cli *Client) handleOwnDevicesNotification(node *waBinary.Node) {
cli.userDevicesCacheLock.Lock()
defer cli.userDevicesCacheLock.Unlock()
@ -146,13 +158,12 @@ func (cli *Client) handleOwnDevicesNotification(node *waBinary.Node) {
cli.Log.Debugf("Ignoring own device change notification, device list not cached")
return
}
oldHash := participantListHashV2(cached)
oldHash := participantListHashV2(cached.devices)
expectedNewHash := node.AttrGetter().String("dhash")
var newDeviceList []types.JID
for _, child := range node.GetChildren() {
jid := child.AttrGetter().JID("jid")
if child.Tag == "device" && !jid.IsEmpty() {
jid.AD = true
newDeviceList = append(newDeviceList, jid)
}
}
@ -162,10 +173,32 @@ func (cli *Client) handleOwnDevicesNotification(node *waBinary.Node) {
delete(cli.userDevicesCache, ownID)
} else {
cli.Log.Debugf("Received own device list change notification %s -> %s", oldHash, newHash)
cli.userDevicesCache[ownID] = newDeviceList
cli.userDevicesCache[ownID] = deviceCache{devices: newDeviceList, dhash: expectedNewHash}
}
}
func (cli *Client) handleBlocklist(node *waBinary.Node) {
ag := node.AttrGetter()
evt := events.Blocklist{
Action: events.BlocklistAction(ag.OptionalString("action")),
DHash: ag.String("dhash"),
PrevDHash: ag.OptionalString("prev_dhash"),
}
for _, child := range node.GetChildren() {
ag := child.AttrGetter()
change := events.BlocklistChange{
JID: ag.JID("jid"),
Action: events.BlocklistChangeAction(ag.String("action")),
}
if !ag.OK() {
cli.Log.Warnf("Unexpected data in blocklist event child %v: %v", child.XMLString(), ag.Error())
continue
}
evt.Changes = append(evt.Changes, change)
}
cli.dispatchEvent(&evt)
}
func (cli *Client) handleAccountSyncNotification(node *waBinary.Node) {
for _, child := range node.GetChildren() {
switch child.Tag {
@ -178,6 +211,8 @@ func (cli *Client) handleAccountSyncNotification(node *waBinary.Node) {
Timestamp: node.AttrGetter().UnixTime("t"),
JID: cli.getOwnID().ToNonAD(),
})
case "blocklist":
cli.handleBlocklist(&child)
default:
cli.Log.Debugf("Unhandled account sync item %s", child.Tag)
}
@ -230,6 +265,93 @@ func (cli *Client) handlePrivacyTokenNotification(node *waBinary.Node) {
}
}
func (cli *Client) parseNewsletterMessages(node *waBinary.Node) []*types.NewsletterMessage {
children := node.GetChildren()
output := make([]*types.NewsletterMessage, 0, len(children))
for _, child := range children {
if child.Tag != "message" {
continue
}
msg := types.NewsletterMessage{
MessageServerID: child.AttrGetter().Int("server_id"),
ViewsCount: 0,
ReactionCounts: nil,
}
for _, subchild := range child.GetChildren() {
switch subchild.Tag {
case "plaintext":
byteContent, ok := subchild.Content.([]byte)
if ok {
msg.Message = new(waProto.Message)
err := proto.Unmarshal(byteContent, msg.Message)
if err != nil {
cli.Log.Warnf("Failed to unmarshal newsletter message: %v", err)
msg.Message = nil
}
}
case "views_count":
msg.ViewsCount = subchild.AttrGetter().Int("count")
case "reactions":
msg.ReactionCounts = make(map[string]int)
for _, reaction := range subchild.GetChildren() {
rag := reaction.AttrGetter()
msg.ReactionCounts[rag.String("code")] = rag.Int("count")
}
}
}
output = append(output, &msg)
}
return output
}
func (cli *Client) handleNewsletterNotification(node *waBinary.Node) {
ag := node.AttrGetter()
liveUpdates := node.GetChildByTag("live_updates")
cli.dispatchEvent(&events.NewsletterLiveUpdate{
JID: ag.JID("from"),
Time: ag.UnixTime("t"),
Messages: cli.parseNewsletterMessages(&liveUpdates),
})
}
type newsLetterEventWrapper struct {
Data newsletterEvent `json:"data"`
}
type newsletterEvent struct {
Join *events.NewsletterJoin `json:"xwa2_notify_newsletter_on_join"`
Leave *events.NewsletterLeave `json:"xwa2_notify_newsletter_on_leave"`
MuteChange *events.NewsletterMuteChange `json:"xwa2_notify_newsletter_on_mute_change"`
// _on_admin_metadata_update -> id, thread_metadata, messages
// _on_metadata_update
// _on_state_change -> id, is_requestor, state
}
func (cli *Client) handleMexNotification(node *waBinary.Node) {
for _, child := range node.GetChildren() {
if child.Tag != "update" {
continue
}
childData, ok := child.Content.([]byte)
if !ok {
continue
}
var wrapper newsLetterEventWrapper
err := json.Unmarshal(childData, &wrapper)
if err != nil {
cli.Log.Errorf("Failed to unmarshal JSON in mex event: %v", err)
continue
}
if wrapper.Data.Join != nil {
cli.dispatchEvent(wrapper.Data.Join)
} else if wrapper.Data.Leave != nil {
cli.dispatchEvent(wrapper.Data.Leave)
} else if wrapper.Data.MuteChange != nil {
cli.dispatchEvent(wrapper.Data.MuteChange)
}
}
}
func (cli *Client) handleNotification(node *waBinary.Node) {
ag := node.AttrGetter()
notifType := ag.String("type")
@ -246,6 +368,8 @@ func (cli *Client) handleNotification(node *waBinary.Node) {
go cli.handleAccountSyncNotification(node)
case "devices":
go cli.handleDeviceNotification(node)
case "fbid:devices":
go cli.handleFBDeviceNotification(node)
case "w:gp2":
evt, err := cli.parseGroupNotification(node)
if err != nil {
@ -261,6 +385,10 @@ func (cli *Client) handleNotification(node *waBinary.Node) {
go cli.handlePrivacyTokenNotification(node)
case "link_code_companion_reg":
go cli.tryHandleCodePairNotification(node)
case "newsletter":
go cli.handleNewsletterNotification(node)
case "mex":
go cli.handleMexNotification(node)
// Other types: business, disappearing_mode, server, status, pay, psa
default:
cli.Log.Debugf("Unhandled notification with type %s", notifType)

View File

@ -15,16 +15,14 @@ import (
"regexp"
"strconv"
"go.mau.fi/util/random"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/pbkdf2"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/hkdfutil"
"go.mau.fi/whatsmeow/util/keys"
"go.mau.fi/whatsmeow/util/randbytes"
)
// PairClientType is the type of client to use with PairCode.
@ -44,29 +42,6 @@ const (
PairClientOtherWebClient
)
func platformTypeToPairClientType(platformType waProto.DeviceProps_PlatformType) PairClientType {
switch platformType {
case waProto.DeviceProps_CHROME:
return PairClientChrome
case waProto.DeviceProps_EDGE:
return PairClientEdge
case waProto.DeviceProps_FIREFOX:
return PairClientFirefox
case waProto.DeviceProps_IE:
return PairClientIE
case waProto.DeviceProps_OPERA:
return PairClientOpera
case waProto.DeviceProps_SAFARI:
return PairClientSafari
case waProto.DeviceProps_DESKTOP:
return PairClientElectron
case waProto.DeviceProps_UWP:
return PairClientUWP
default:
return PairClientOtherWebClient
}
}
var notNumbers = regexp.MustCompile("[^0-9]")
var linkingBase32 = base32.NewEncoding("123456789ABCDEFGHJKLMNPQRSTVWXYZ")
@ -79,9 +54,9 @@ type phoneLinkingCache struct {
func generateCompanionEphemeralKey() (ephemeralKeyPair *keys.KeyPair, ephemeralKey []byte, encodedLinkingCode string) {
ephemeralKeyPair = keys.NewKeyPair()
salt := randbytes.Make(32)
iv := randbytes.Make(16)
linkingCode := randbytes.Make(5)
salt := random.Bytes(32)
iv := random.Bytes(16)
linkingCode := random.Bytes(5)
encodedLinkingCode = linkingBase32.EncodeToString(linkingCode)
linkCodeKey := pbkdf2.Key([]byte(encodedLinkingCode), salt, 2<<16, 32, sha256.New)
linkCipherBlock, _ := aes.NewCipher(linkCodeKey)
@ -96,15 +71,19 @@ func generateCompanionEphemeralKey() (ephemeralKeyPair *keys.KeyPair, ephemeralK
// PairPhone generates a pairing code that can be used to link to a phone without scanning a QR code.
//
// You must connect the client normally before calling this (which means you'll also receive a QR code
// event, but that can be ignored when doing code pairing).
//
// The exact expiry of pairing codes is unknown, but QR codes are always generated and the login websocket is closed
// after the QR codes run out, which means there's a 160-second time limit. It is recommended to generate the pairing
// code immediately after connecting to the websocket to have the maximum time.
//
// The clientType parameter must be one of the PairClient* constants, but which one doesn't matter.
// The client display name must be formatted as `Browser (OS)`, and only common browsers/OSes are allowed
// (the server will validate it and return 400 if it's wrong).
//
// See https://faq.whatsapp.com/1324084875126592 for more info
func (cli *Client) PairPhone(phone string, showPushNotification bool) (string, error) {
clientType := platformTypeToPairClientType(store.DeviceProps.GetPlatformType())
clientDisplayName := store.DeviceProps.GetOs()
func (cli *Client) PairPhone(phone string, showPushNotification bool, clientType PairClientType, clientDisplayName string) (string, error) {
ephemeralKeyPair, ephemeralKey, encodedLinkingCode := generateCompanionEphemeralKey()
phone = notNumbers.ReplaceAllString(phone, "")
jid := types.NewJID(phone, types.DefaultUserServer)
@ -187,9 +166,9 @@ func (cli *Client) handleCodePairNotification(parentNode *waBinary.Node) error {
}
}
advSecretRandom := randbytes.Make(32)
keyBundleSalt := randbytes.Make(32)
keyBundleNonce := randbytes.Make(12)
advSecretRandom := random.Bytes(32)
keyBundleSalt := random.Bytes(32)
keyBundleNonce := random.Bytes(12)
// Decrypt the primary device's ephemeral public key, which was encrypted with the 8-character pairing code,
// then compute the DH shared secret using our ephemeral private key we generated earlier.

View File

@ -125,7 +125,6 @@ func (cli *Client) fetchPreKeys(ctx context.Context, users []types.JID) (map[typ
continue
}
jid := child.AttrGetter().JID("jid")
jid.AD = true
bundle, err := nodeToPreKeyBundle(uint32(jid.Device), child)
respData[jid] = preKeyResp{bundle, err}
}

View File

@ -8,7 +8,6 @@ package whatsmeow
import (
"fmt"
"sync/atomic"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
@ -66,9 +65,9 @@ func (cli *Client) SendPresence(state types.Presence) error {
return ErrNoPushName
}
if state == types.PresenceAvailable {
atomic.CompareAndSwapUint32(&cli.sendActiveReceipts, 0, 1)
cli.sendActiveReceipts.CompareAndSwap(0, 1)
} else {
atomic.CompareAndSwapUint32(&cli.sendActiveReceipts, 1, 0)
cli.sendActiveReceipts.CompareAndSwap(1, 0)
}
return cli.sendNode(waBinary.Node{
Tag: "presence",

View File

@ -7,6 +7,9 @@
package whatsmeow
import (
"strconv"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
@ -39,6 +42,9 @@ func (cli *Client) TryFetchPrivacySettings(ignoreCache bool) (*types.PrivacySett
// GetPrivacySettings will get the user's privacy settings. If an error occurs while fetching them, the error will be
// logged, but the method will just return an empty struct.
func (cli *Client) GetPrivacySettings() (settings types.PrivacySettings) {
if cli.MessengerConfig != nil {
return
}
settingsPtr, err := cli.TryFetchPrivacySettings(false)
if err != nil {
cli.Log.Errorf("Failed to fetch privacy settings: %v", err)
@ -48,6 +54,69 @@ func (cli *Client) GetPrivacySettings() (settings types.PrivacySettings) {
return
}
// SetPrivacySetting will set the given privacy setting to the given value.
// The privacy settings will be fetched from the server after the change and the new settings will be returned.
// If an error occurs while fetching the new settings, will return an empty struct.
func (cli *Client) SetPrivacySetting(name types.PrivacySettingType, value types.PrivacySetting) (settings types.PrivacySettings, err error) {
settingsPtr, err := cli.TryFetchPrivacySettings(false)
if err != nil {
return settings, err
}
_, err = cli.sendIQ(infoQuery{
Namespace: "privacy",
Type: iqSet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "privacy",
Content: []waBinary.Node{{
Tag: "category",
Attrs: waBinary.Attrs{
"name": string(name),
"value": string(value),
},
}},
}},
})
if err != nil {
return settings, err
}
settings = *settingsPtr
switch name {
case types.PrivacySettingTypeGroupAdd:
settings.GroupAdd = value
case types.PrivacySettingTypeLastSeen:
settings.LastSeen = value
case types.PrivacySettingTypeStatus:
settings.Status = value
case types.PrivacySettingTypeProfile:
settings.Profile = value
case types.PrivacySettingTypeReadReceipts:
settings.ReadReceipts = value
case types.PrivacySettingTypeOnline:
settings.Online = value
case types.PrivacySettingTypeCallAdd:
settings.CallAdd = value
}
cli.privacySettingsCache.Store(&settings)
return
}
// SetDefaultDisappearingTimer will set the default disappearing message timer.
func (cli *Client) SetDefaultDisappearingTimer(timer time.Duration) (err error) {
_, err = cli.sendIQ(infoQuery{
Namespace: "disappearing_mode",
Type: iqSet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "disappearing_mode",
Attrs: waBinary.Attrs{
"duration": strconv.Itoa(int(timer.Seconds())),
},
}},
})
return
}
func (cli *Client) parsePrivacySettings(privacyNode *waBinary.Node, settings *types.PrivacySettings) *events.PrivacySettings {
var evt events.PrivacySettings
for _, child := range privacyNode.GetChildren() {
@ -55,24 +124,30 @@ func (cli *Client) parsePrivacySettings(privacyNode *waBinary.Node, settings *ty
continue
}
ag := child.AttrGetter()
name := ag.String("name")
name := types.PrivacySettingType(ag.String("name"))
value := types.PrivacySetting(ag.String("value"))
switch name {
case "groupadd":
case types.PrivacySettingTypeGroupAdd:
settings.GroupAdd = value
evt.GroupAddChanged = true
case "last":
case types.PrivacySettingTypeLastSeen:
settings.LastSeen = value
evt.LastSeenChanged = true
case "status":
case types.PrivacySettingTypeStatus:
settings.Status = value
evt.StatusChanged = true
case "profile":
case types.PrivacySettingTypeProfile:
settings.Profile = value
evt.ProfileChanged = true
case "readreceipts":
case types.PrivacySettingTypeReadReceipts:
settings.ReadReceipts = value
evt.ReadReceiptsChanged = true
case types.PrivacySettingTypeOnline:
settings.Online = value
evt.OnlineChanged = true
case types.PrivacySettingTypeCallAdd:
settings.CallAdd = value
evt.CallAddChanged = true
}
}
return &evt
@ -83,6 +158,7 @@ func (cli *Client) handlePrivacySettingsNotification(privacyNode *waBinary.Node)
settings, err := cli.TryFetchPrivacySettings(false)
if err != nil {
cli.Log.Errorf("Failed to fetch privacy settings when handling change: %v", err)
return
}
evt := cli.parsePrivacySettings(privacyNode, settings)
// The data isn't be reliable if the fetch failed, so only cache if it didn't fail

View File

@ -8,7 +8,6 @@ package whatsmeow
import (
"fmt"
"sync/atomic"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
@ -21,7 +20,7 @@ func (cli *Client) handleReceipt(node *waBinary.Node) {
if err != nil {
cli.Log.Warnf("Failed to parse receipt: %v", err)
} else if receipt != nil {
if receipt.Type == events.ReceiptTypeRetry {
if receipt.Type == types.ReceiptTypeRetry {
go func() {
err := cli.handleRetryReceipt(receipt, node)
if err != nil {
@ -63,7 +62,7 @@ func (cli *Client) parseReceipt(node *waBinary.Node) (*events.Receipt, error) {
receipt := events.Receipt{
MessageSource: source,
Timestamp: ag.UnixTime("t"),
Type: events.ReceiptType(ag.OptionalString("type")),
Type: types.ReceiptType(ag.OptionalString("type")),
}
if source.IsGroup && source.Sender.IsEmpty() {
participantTags := node.GetChildrenByTag("participants")
@ -127,20 +126,36 @@ func (cli *Client) sendAck(node *waBinary.Node) {
//
// You can mark multiple messages as read at the same time, but only if the messages were sent by the same user.
// To mark messages by different users as read, you must call MarkRead multiple times (once for each user).
func (cli *Client) MarkRead(ids []types.MessageID, timestamp time.Time, chat, sender types.JID) error {
//
// To mark a voice message as played, specify types.ReceiptTypePlayed as the last parameter.
// Providing more than one receipt type will panic: the parameter is only a vararg for backwards compatibility.
func (cli *Client) MarkRead(ids []types.MessageID, timestamp time.Time, chat, sender types.JID, receiptTypeExtra ...types.ReceiptType) error {
if len(ids) == 0 {
return fmt.Errorf("no message IDs specified")
}
receiptType := types.ReceiptTypeRead
if len(receiptTypeExtra) == 1 {
receiptType = receiptTypeExtra[0]
} else if len(receiptTypeExtra) > 1 {
panic(fmt.Errorf("too many receipt types specified"))
}
node := waBinary.Node{
Tag: "receipt",
Attrs: waBinary.Attrs{
"id": ids[0],
"type": "read",
"type": string(receiptType),
"to": chat,
"t": timestamp.Unix(),
},
}
if cli.GetPrivacySettings().ReadReceipts == types.PrivacySettingNone {
node.Attrs["type"] = "read-self"
if chat.Server == types.NewsletterServer || cli.GetPrivacySettings().ReadReceipts == types.PrivacySettingNone {
switch receiptType {
case types.ReceiptTypeRead:
node.Attrs["type"] = string(types.ReceiptTypeReadSelf)
// TODO change played to played-self?
}
}
if !sender.IsEmpty() && chat.Server != types.DefaultUserServer {
if !sender.IsEmpty() && chat.Server != types.DefaultUserServer && chat.Server != types.MessengerServer {
node.Attrs["participant"] = sender.ToNonAD()
}
if len(ids) > 1 {
@ -174,9 +189,9 @@ func (cli *Client) MarkRead(ids []types.MessageID, timestamp time.Time, chat, se
// receipts will act like the client is offline until SendPresence is called again.
func (cli *Client) SetForceActiveDeliveryReceipts(active bool) {
if active {
atomic.StoreUint32(&cli.sendActiveReceipts, 2)
cli.sendActiveReceipts.Store(2)
} else {
atomic.StoreUint32(&cli.sendActiveReceipts, 0)
cli.sendActiveReceipts.Store(0)
}
}
@ -185,9 +200,9 @@ func (cli *Client) sendMessageReceipt(info *types.MessageInfo) {
"id": info.ID,
}
if info.IsFromMe {
attrs["type"] = "sender"
} else if atomic.LoadUint32(&cli.sendActiveReceipts) == 0 {
attrs["type"] = "inactive"
attrs["type"] = string(types.ReceiptTypeSender)
} else if cli.sendActiveReceipts.Load() == 0 {
attrs["type"] = string(types.ReceiptTypeInactive)
}
attrs["to"] = info.Chat
if info.IsGroup {

View File

@ -10,7 +10,6 @@ import (
"context"
"fmt"
"strconv"
"sync/atomic"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
@ -18,7 +17,7 @@ import (
)
func (cli *Client) generateRequestID() string {
return cli.uniqueID + strconv.FormatUint(uint64(atomic.AddUint32(&cli.idCounter, 1)), 10)
return cli.uniqueID + strconv.FormatUint(cli.idCounter.Add(1), 10)
}
var xmlStreamEndNode = &waBinary.Node{Tag: "xmlstreamend"}
@ -139,13 +138,15 @@ func (cli *Client) sendIQAsync(query infoQuery) (<-chan *waBinary.Node, error) {
return ch, err
}
const defaultRequestTimeout = 75 * time.Second
func (cli *Client) sendIQ(query infoQuery) (*waBinary.Node, error) {
resChan, data, err := cli.sendIQAsyncAndGetData(&query)
if err != nil {
return nil, err
}
if query.Timeout == 0 {
query.Timeout = 75 * time.Second
query.Timeout = defaultRequestTimeout
}
if query.Context == nil {
query.Context = context.Background()

View File

@ -8,6 +8,8 @@ package whatsmeow
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"fmt"
"time"
@ -19,6 +21,10 @@ import (
"google.golang.org/protobuf/proto"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/binary/armadillo/waCommon"
"go.mau.fi/whatsmeow/binary/armadillo/waConsumerApplication"
"go.mau.fi/whatsmeow/binary/armadillo/waMsgApplication"
"go.mau.fi/whatsmeow/binary/armadillo/waMsgTransport"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
@ -32,19 +38,22 @@ type recentMessageKey struct {
ID types.MessageID
}
// RecentMessage contains the info needed to re-send a message when another device fails to decrypt it.
type RecentMessage struct {
Proto *waProto.Message
Timestamp time.Time
wa *waProto.Message
fb *waMsgApplication.MessageApplication
}
func (cli *Client) addRecentMessage(to types.JID, id types.MessageID, message *waProto.Message) {
func (rm RecentMessage) IsEmpty() bool {
return rm.wa == nil && rm.fb == nil
}
func (cli *Client) addRecentMessage(to types.JID, id types.MessageID, wa *waProto.Message, fb *waMsgApplication.MessageApplication) {
cli.recentMessagesLock.Lock()
key := recentMessageKey{to, id}
if cli.recentMessagesList[cli.recentMessagesPtr].ID != "" {
delete(cli.recentMessagesMap, cli.recentMessagesList[cli.recentMessagesPtr])
}
cli.recentMessagesMap[key] = message
cli.recentMessagesMap[key] = RecentMessage{wa: wa, fb: fb}
cli.recentMessagesList[cli.recentMessagesPtr] = key
cli.recentMessagesPtr++
if cli.recentMessagesPtr >= len(cli.recentMessagesList) {
@ -53,26 +62,27 @@ func (cli *Client) addRecentMessage(to types.JID, id types.MessageID, message *w
cli.recentMessagesLock.Unlock()
}
func (cli *Client) getRecentMessage(to types.JID, id types.MessageID) *waProto.Message {
func (cli *Client) getRecentMessage(to types.JID, id types.MessageID) RecentMessage {
cli.recentMessagesLock.RLock()
msg, _ := cli.recentMessagesMap[recentMessageKey{to, id}]
cli.recentMessagesLock.RUnlock()
return msg
}
func (cli *Client) getMessageForRetry(receipt *events.Receipt, messageID types.MessageID) (*waProto.Message, error) {
func (cli *Client) getMessageForRetry(receipt *events.Receipt, messageID types.MessageID) (RecentMessage, error) {
msg := cli.getRecentMessage(receipt.Chat, messageID)
if msg == nil {
msg = cli.GetMessageForRetry(receipt.Sender, receipt.Chat, messageID)
if msg == nil {
return nil, fmt.Errorf("couldn't find message %s", messageID)
if msg.IsEmpty() {
waMsg := cli.GetMessageForRetry(receipt.Sender, receipt.Chat, messageID)
if waMsg == nil {
return RecentMessage{}, fmt.Errorf("couldn't find message %s", messageID)
} else {
cli.Log.Debugf("Found message in GetMessageForRetry to accept retry receipt for %s/%s from %s", receipt.Chat, messageID, receipt.Sender)
}
msg = RecentMessage{wa: waMsg}
} else {
cli.Log.Debugf("Found message in local cache to accept retry receipt for %s/%s from %s", receipt.Chat, messageID, receipt.Sender)
}
return proto.Clone(msg).(*waProto.Message), nil
return msg, nil
}
const recreateSessionTimeout = 1 * time.Hour
@ -94,6 +104,11 @@ func (cli *Client) shouldRecreateSession(retryCount int, jid types.JID) (reason
return "", false
}
type incomingRetryKey struct {
jid types.JID
messageID types.MessageID
}
// handleRetryReceipt handles an incoming retry receipt for an outgoing message.
func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.Node) error {
retryChild, ok := node.GetOptionalChildByTag("retry")
@ -111,40 +126,87 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
if err != nil {
return err
}
var fbConsumerMsg *waConsumerApplication.ConsumerApplication
if msg.fb != nil {
subProto, ok := msg.fb.GetPayload().GetSubProtocol().GetSubProtocol().(*waMsgApplication.MessageApplication_SubProtocolPayload_ConsumerMessage)
if ok {
fbConsumerMsg, err = subProto.Decode()
if err != nil {
return fmt.Errorf("failed to decode consumer message for retry: %w", err)
}
}
}
retryKey := incomingRetryKey{receipt.Sender, messageID}
cli.incomingRetryRequestCounterLock.Lock()
cli.incomingRetryRequestCounter[retryKey]++
internalCounter := cli.incomingRetryRequestCounter[retryKey]
cli.incomingRetryRequestCounterLock.Unlock()
if internalCounter >= 10 {
cli.Log.Warnf("Dropping retry request from %s for %s: internal retry counter is %d", messageID, receipt.Sender, internalCounter)
return nil
}
ownID := cli.getOwnID()
if ownID.IsEmpty() {
return ErrNotLoggedIn
}
var fbSKDM *waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage
var fbDSM *waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage
if receipt.IsGroup {
builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer)
senderKeyName := protocol.NewSenderKeyName(receipt.Chat.String(), ownID.SignalAddress())
signalSKDMessage, err := builder.Create(senderKeyName)
if err != nil {
cli.Log.Warnf("Failed to create sender key distribution message to include in retry of %s in %s to %s: %v", messageID, receipt.Chat, receipt.Sender, err)
} else {
msg.SenderKeyDistributionMessage = &waProto.SenderKeyDistributionMessage{
}
if msg.wa != nil {
msg.wa.SenderKeyDistributionMessage = &waProto.SenderKeyDistributionMessage{
GroupId: proto.String(receipt.Chat.String()),
AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(),
}
} else {
fbSKDM = &waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage{
GroupID: receipt.Chat.String(),
AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(),
}
}
} else if receipt.IsFromMe {
msg = &waProto.Message{
DeviceSentMessage: &waProto.DeviceSentMessage{
DestinationJid: proto.String(receipt.Chat.String()),
Message: msg,
},
if msg.wa != nil {
msg.wa = &waProto.Message{
DeviceSentMessage: &waProto.DeviceSentMessage{
DestinationJid: proto.String(receipt.Chat.String()),
Message: msg.wa,
},
}
} else {
fbDSM = &waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage{
DestinationJID: receipt.Chat.String(),
}
}
}
if cli.PreRetryCallback != nil && !cli.PreRetryCallback(receipt, messageID, retryCount, msg) {
// TODO pre-retry callback for fb
if cli.PreRetryCallback != nil && !cli.PreRetryCallback(receipt, messageID, retryCount, msg.wa) {
cli.Log.Debugf("Cancelled retry receipt in PreRetryCallback")
return nil
}
plaintext, err := proto.Marshal(msg)
if err != nil {
return fmt.Errorf("failed to marshal message: %w", err)
var plaintext, frankingTag []byte
if msg.wa != nil {
plaintext, err = proto.Marshal(msg.wa)
if err != nil {
return fmt.Errorf("failed to marshal message: %w", err)
}
} else {
plaintext, err = proto.Marshal(msg.fb)
if err != nil {
return fmt.Errorf("failed to marshal consumer message: %w", err)
}
frankingHash := hmac.New(sha256.New, msg.fb.GetMetadata().GetFrankingKey())
frankingHash.Write(plaintext)
frankingTag = frankingHash.Sum(nil)
}
_, hasKeys := node.GetOptionalChildByTag("keys")
var bundle *prekey.Bundle
@ -160,20 +222,39 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
if err != nil {
return err
}
senderAD := receipt.Sender
senderAD.AD = true
bundle, err = keys[senderAD].bundle, keys[senderAD].err
bundle, err = keys[receipt.Sender].bundle, keys[receipt.Sender].err
if err != nil {
return fmt.Errorf("failed to fetch prekeys: %w", err)
} else if bundle == nil {
return fmt.Errorf("didn't get prekey bundle for %s (response size: %d)", senderAD, len(keys))
return fmt.Errorf("didn't get prekey bundle for %s (response size: %d)", receipt.Sender, len(keys))
}
}
encAttrs := waBinary.Attrs{}
if mediaType := getMediaTypeFromMessage(msg); mediaType != "" {
encAttrs["mediatype"] = mediaType
var msgAttrs messageAttrs
if msg.wa != nil {
msgAttrs.MediaType = getMediaTypeFromMessage(msg.wa)
msgAttrs.Type = getTypeFromMessage(msg.wa)
} else if fbConsumerMsg != nil {
msgAttrs = getAttrsFromFBMessage(fbConsumerMsg)
} else {
msgAttrs.Type = "text"
}
if msgAttrs.MediaType != "" {
encAttrs["mediatype"] = msgAttrs.MediaType
}
var encrypted *waBinary.Node
var includeDeviceIdentity bool
if msg.wa != nil {
encrypted, includeDeviceIdentity, err = cli.encryptMessageForDevice(plaintext, receipt.Sender, bundle, encAttrs)
} else {
encrypted, err = cli.encryptMessageForDeviceV3(&waMsgTransport.MessageTransport_Payload{
ApplicationPayload: &waCommon.SubProtocol{
Payload: plaintext,
Version: FBMessageApplicationVersion,
},
FutureProof: waCommon.FutureProofBehavior_PLACEHOLDER,
}, fbSKDM, fbDSM, receipt.Sender, bundle, encAttrs)
}
encrypted, includeDeviceIdentity, err := cli.encryptMessageForDevice(plaintext, receipt.Sender, bundle, encAttrs)
if err != nil {
return fmt.Errorf("failed to encrypt message for retry: %w", err)
}
@ -181,7 +262,7 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
attrs := waBinary.Attrs{
"to": node.Attrs["from"],
"type": getTypeFromMessage(msg),
"type": msgAttrs.Type,
"id": messageID,
"t": timestamp.Unix(),
}
@ -197,10 +278,19 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
if edit, ok := node.Attrs["edit"]; ok {
attrs["edit"] = edit
}
var content []waBinary.Node
if msg.wa != nil {
content = cli.getMessageContent(*encrypted, msg.wa, attrs, includeDeviceIdentity)
} else {
content = []waBinary.Node{
*encrypted,
{Tag: "franking", Content: []waBinary.Node{{Tag: "franking_tag", Content: frankingTag}}},
}
}
err = cli.sendNode(waBinary.Node{
Tag: "message",
Attrs: attrs,
Content: cli.getMessageContent(*encrypted, msg, attrs, includeDeviceIdentity),
Content: content,
})
if err != nil {
return fmt.Errorf("failed to send retry message: %w", err)
@ -210,7 +300,7 @@ func (cli *Client) handleRetryReceipt(receipt *events.Receipt, node *waBinary.No
}
func (cli *Client) cancelDelayedRequestFromPhone(msgID types.MessageID) {
if !cli.AutomaticMessageRerequestFromPhone {
if !cli.AutomaticMessageRerequestFromPhone || cli.MessengerConfig != nil {
return
}
cli.pendingPhoneRerequestsLock.RLock()
@ -226,7 +316,7 @@ func (cli *Client) cancelDelayedRequestFromPhone(msgID types.MessageID) {
var RequestFromPhoneDelay = 5 * time.Second
func (cli *Client) delayedRequestMessageFromPhone(info *types.MessageInfo) {
if !cli.AutomaticMessageRerequestFromPhone {
if !cli.AutomaticMessageRerequestFromPhone || cli.MessengerConfig != nil {
return
}
cli.pendingPhoneRerequestsLock.Lock()
@ -253,7 +343,7 @@ func (cli *Client) delayedRequestMessageFromPhone(info *types.MessageInfo) {
}
_, err := cli.SendMessage(
ctx,
cli.Store.ID.ToNonAD(),
cli.getOwnID().ToNonAD(),
cli.BuildUnavailableMessageRequest(info.Chat, info.Sender, info.ID),
SendRequestExtra{Peer: true},
)

View File

@ -19,19 +19,19 @@ import (
"strings"
"time"
"go.mau.fi/libsignal/signalerror"
"google.golang.org/protobuf/proto"
"github.com/rs/zerolog"
"go.mau.fi/libsignal/groups"
"go.mau.fi/libsignal/keys/prekey"
"go.mau.fi/libsignal/protocol"
"go.mau.fi/libsignal/session"
"go.mau.fi/libsignal/signalerror"
"go.mau.fi/util/random"
"google.golang.org/protobuf/proto"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/randbytes"
)
// GenerateMessageID generates a random string that can be used as a message ID on WhatsApp.
@ -39,6 +39,9 @@ import (
// msgID := cli.GenerateMessageID()
// cli.SendMessage(context.Background(), targetJID, &waProto.Message{...}, whatsmeow.SendRequestExtra{ID: msgID})
func (cli *Client) GenerateMessageID() types.MessageID {
if cli.MessengerConfig != nil {
return types.MessageID(strconv.FormatInt(GenerateFacebookMessageID(), 10))
}
data := make([]byte, 8, 8+20+16)
binary.BigEndian.PutUint64(data, uint64(time.Now().Unix()))
ownID := cli.getOwnID()
@ -46,11 +49,16 @@ func (cli *Client) GenerateMessageID() types.MessageID {
data = append(data, []byte(ownID.User)...)
data = append(data, []byte("@c.us")...)
}
data = append(data, randbytes.Make(16)...)
data = append(data, random.Bytes(16)...)
hash := sha256.Sum256(data)
return "3EB0" + strings.ToUpper(hex.EncodeToString(hash[:9]))
}
func GenerateFacebookMessageID() int64 {
const randomMask = (1 << 22) - 1
return (time.Now().UnixMilli() << 22) | (int64(binary.BigEndian.Uint32(random.Bytes(4))) & randomMask)
}
// GenerateMessageID generates a random string that can be used as a message ID on WhatsApp.
//
// msgID := whatsmeow.GenerateMessageID()
@ -58,7 +66,7 @@ func (cli *Client) GenerateMessageID() types.MessageID {
//
// Deprecated: WhatsApp web has switched to using a hash of the current timestamp, user id and random bytes. Use Client.GenerateMessageID instead.
func GenerateMessageID() types.MessageID {
return "3EB0" + strings.ToUpper(hex.EncodeToString(randbytes.Make(8)))
return "3EB0" + strings.ToUpper(hex.EncodeToString(random.Bytes(8)))
}
type MessageDebugTimings struct {
@ -75,6 +83,24 @@ type MessageDebugTimings struct {
Retry time.Duration
}
func (mdt MessageDebugTimings) MarshalZerologObject(evt *zerolog.Event) {
evt.Dur("queue", mdt.Queue)
evt.Dur("marshal", mdt.Marshal)
if mdt.GetParticipants != 0 {
evt.Dur("get_participants", mdt.GetParticipants)
}
evt.Dur("get_devices", mdt.GetDevices)
if mdt.GroupEncrypt != 0 {
evt.Dur("group_encrypt", mdt.GroupEncrypt)
}
evt.Dur("peer_encrypt", mdt.PeerEncrypt)
evt.Dur("send", mdt.Send)
evt.Dur("resp", mdt.Resp)
if mdt.Retry != 0 {
evt.Dur("retry", mdt.Retry)
}
}
type SendResponse struct {
// The message timestamp returned by the server
Timestamp time.Time
@ -82,6 +108,9 @@ type SendResponse struct {
// The ID of the sent message
ID types.MessageID
// The server-specified ID of the sent message. Only present for newsletter messages.
ServerID types.MessageServerID
// Message handling duration, used for debugging
DebugTimings MessageDebugTimings
}
@ -102,6 +131,12 @@ type SendRequestExtra struct {
ID types.MessageID
// Should the message be sent as a peer message (protocol messages to your own devices, e.g. app state key requests)
Peer bool
// A timeout for the send request. Unlike timeouts using the context parameter, this only applies
// to the actual response waiting and not preparing/encrypting the message.
// Defaults to 75 seconds. The timeout can be disabled by using a negative value.
Timeout time.Duration
// When sending media to newsletters, the Handle field returned by the file upload.
MediaHandle string
}
// SendMessage sends the given message.
@ -136,7 +171,7 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waPro
} else if len(extra) == 1 {
req = extra[0]
}
if to.AD && !req.Peer {
if to.Device > 0 && !req.Peer {
err = ErrRecipientADJID
return
}
@ -146,9 +181,20 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waPro
return
}
if req.Timeout == 0 {
req.Timeout = defaultRequestTimeout
}
if len(req.ID) == 0 {
req.ID = cli.GenerateMessageID()
}
if to.Server == types.NewsletterServer {
// TODO somehow deduplicate this with the code in sendNewsletter?
if message.EditedMessage != nil {
req.ID = types.MessageID(message.GetEditedMessage().GetMessage().GetProtocolMessage().GetKey().GetId())
} else if message.ProtocolMessage != nil && message.ProtocolMessage.GetType() == waProto.ProtocolMessage_REVOKE {
req.ID = types.MessageID(message.GetProtocolMessage().GetKey().GetId())
}
}
resp.ID = req.ID
start := time.Now()
@ -160,7 +206,7 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waPro
respChan := cli.waitResponse(req.ID)
// Peer message retries aren't implemented yet
if !req.Peer {
cli.addRecentMessage(to, req.ID, message)
cli.addRecentMessage(to, req.ID, message, nil)
}
if message.GetMessageContextInfo().GetMessageSecret() != nil {
err = cli.Store.MsgSecrets.PutMessageSecret(to, ownID, req.ID, message.GetMessageContextInfo().GetMessageSecret())
@ -181,6 +227,8 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waPro
} else {
data, err = cli.sendDM(ctx, to, ownID, req.ID, message, &resp.DebugTimings)
}
case types.NewsletterServer:
data, err = cli.sendNewsletter(to, req.ID, message, req.MediaHandle, &resp.DebugTimings)
default:
err = fmt.Errorf("%w %s", ErrUnknownServer, to.Server)
}
@ -190,9 +238,20 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waPro
return
}
var respNode *waBinary.Node
var timeoutChan <-chan time.Time
if req.Timeout > 0 {
timeoutChan = time.After(req.Timeout)
} else {
timeoutChan = make(<-chan time.Time)
}
select {
case respNode = <-respChan:
case <-timeoutChan:
cli.cancelResponse(req.ID, respChan)
err = ErrMessageTimedOut
return
case <-ctx.Done():
cli.cancelResponse(req.ID, respChan)
err = ctx.Err()
return
}
@ -206,6 +265,7 @@ func (cli *Client) SendMessage(ctx context.Context, to types.JID, message *waPro
}
}
ag := respNode.AttrGetter()
resp.ServerID = types.MessageServerID(ag.OptionalInt("server_id"))
resp.Timestamp = ag.UnixTime("t")
if errorCode := ag.Int("error"); errorCode != 0 {
err = fmt.Errorf("%w %d", ErrServerReturnedError, errorCode)
@ -241,7 +301,7 @@ func (cli *Client) BuildMessageKey(chat, sender types.JID, id types.MessageID) *
}
if !sender.IsEmpty() && sender.User != cli.getOwnID().User {
key.FromMe = proto.Bool(false)
if chat.Server != types.DefaultUserServer {
if chat.Server != types.DefaultUserServer && chat.Server != types.MessengerServer {
key.Participant = proto.String(sender.ToNonAD().String())
}
}
@ -271,6 +331,8 @@ func (cli *Client) BuildRevoke(chat, sender types.JID, id types.MessageID) *waPr
// The built message can be sent normally using Client.SendMessage.
//
// resp, err := cli.SendMessage(context.Background(), chat, cli.BuildReaction(chat, senderJID, targetMessageID, "🐈️")
//
// Note that for newsletter messages, you need to use NewsletterSendReaction instead of BuildReaction + SendMessage.
func (cli *Client) BuildReaction(chat, sender types.JID, id types.MessageID, reaction string) *waProto.Message {
return &waProto.Message{
ReactionMessage: &waProto.ReactionMessage{
@ -417,7 +479,7 @@ func (cli *Client) SetDisappearingTimer(chat types.JID, timer time.Duration) (er
func participantListHashV2(participants []types.JID) string {
participantsStrings := make([]string, len(participants))
for i, part := range participants {
participantsStrings[i] = part.String()
participantsStrings[i] = part.ADString()
}
sort.Strings(participantsStrings)
@ -425,6 +487,50 @@ func participantListHashV2(participants []types.JID) string {
return fmt.Sprintf("2:%s", base64.RawStdEncoding.EncodeToString(hash[:6]))
}
func (cli *Client) sendNewsletter(to types.JID, id types.MessageID, message *waProto.Message, mediaID string, timings *MessageDebugTimings) ([]byte, error) {
attrs := waBinary.Attrs{
"to": to,
"id": id,
"type": getTypeFromMessage(message),
}
if mediaID != "" {
attrs["media_id"] = mediaID
}
if message.EditedMessage != nil {
attrs["edit"] = string(types.EditAttributeAdminEdit)
message = message.GetEditedMessage().GetMessage().GetProtocolMessage().GetEditedMessage()
} else if message.ProtocolMessage != nil && message.ProtocolMessage.GetType() == waProto.ProtocolMessage_REVOKE {
attrs["edit"] = string(types.EditAttributeAdminRevoke)
message = nil
}
start := time.Now()
plaintext, _, err := marshalMessage(to, message)
timings.Marshal = time.Since(start)
if err != nil {
return nil, err
}
plaintextNode := waBinary.Node{
Tag: "plaintext",
Content: plaintext,
Attrs: waBinary.Attrs{},
}
if mediaType := getMediaTypeFromMessage(message); mediaType != "" {
plaintextNode.Attrs["mediatype"] = mediaType
}
node := waBinary.Node{
Tag: "message",
Attrs: attrs,
Content: []waBinary.Node{plaintextNode},
}
start = time.Now()
data, err := cli.sendNodeAndGetData(node)
timings.Send = time.Since(start)
if err != nil {
return nil, fmt.Errorf("failed to send message node: %w", err)
}
return data, nil
}
func (cli *Client) sendGroup(ctx context.Context, to, ownID types.JID, id types.MessageID, message *waProto.Message, timings *MessageDebugTimings) (string, []byte, error) {
var participants []types.JID
var err error
@ -653,16 +759,9 @@ func getButtonAttributes(msg *waProto.Message) waBinary.Attrs {
}
}
const (
EditAttributeEmpty = ""
EditAttributeMessageEdit = "1"
EditAttributeSenderRevoke = "7"
EditAttributeAdminRevoke = "8"
)
const RemoveReactionText = ""
func getEditAttribute(msg *waProto.Message) string {
func getEditAttribute(msg *waProto.Message) types.EditAttribute {
switch {
case msg.EditedMessage != nil && msg.EditedMessage.Message != nil:
return getEditAttribute(msg.EditedMessage.Message)
@ -670,21 +769,21 @@ func getEditAttribute(msg *waProto.Message) string {
switch msg.ProtocolMessage.GetType() {
case waProto.ProtocolMessage_REVOKE:
if msg.ProtocolMessage.GetKey().GetFromMe() {
return EditAttributeSenderRevoke
return types.EditAttributeSenderRevoke
} else {
return EditAttributeAdminRevoke
return types.EditAttributeAdminRevoke
}
case waProto.ProtocolMessage_MESSAGE_EDIT:
if msg.ProtocolMessage.EditedMessage != nil {
return EditAttributeMessageEdit
return types.EditAttributeMessageEdit
}
}
case msg.ReactionMessage != nil && msg.ReactionMessage.GetText() == RemoveReactionText:
return EditAttributeSenderRevoke
return types.EditAttributeSenderRevoke
case msg.KeepInChatMessage != nil && msg.KeepInChatMessage.GetKey().GetFromMe() && msg.KeepInChatMessage.GetKeepType() == waProto.KeepType_UNDO_KEEP_FOR_ALL:
return EditAttributeSenderRevoke
return types.EditAttributeSenderRevoke
}
return EditAttributeEmpty
return types.EditAttributeEmpty
}
func (cli *Client) preparePeerMessageNode(to types.JID, id types.MessageID, message *waProto.Message, timings *MessageDebugTimings) (*waBinary.Node, error) {
@ -711,7 +810,7 @@ func (cli *Client) preparePeerMessageNode(to types.JID, id types.MessageID, mess
return nil, fmt.Errorf("failed to encrypt peer message for %s: %v", to, err)
}
content := []waBinary.Node{*encrypted}
if isPreKey {
if isPreKey && cli.MessengerConfig == nil {
content = append(content, cli.makeDeviceIdentityNode())
}
return &waBinary.Node{
@ -770,10 +869,10 @@ func (cli *Client) prepareMessageNode(ctx context.Context, to, ownID types.JID,
"to": to,
}
if editAttr := getEditAttribute(message); editAttr != "" {
attrs["edit"] = editAttr
attrs["edit"] = string(editAttr)
encAttrs["decrypt-fail"] = string(events.DecryptFailHide)
}
if msgType == "reaction" {
if msgType == "reaction" || message.GetPollUpdateMessage() != nil {
encAttrs["decrypt-fail"] = string(events.DecryptFailHide)
}
@ -792,13 +891,16 @@ func (cli *Client) prepareMessageNode(ctx context.Context, to, ownID types.JID,
}
func marshalMessage(to types.JID, message *waProto.Message) (plaintext, dsmPlaintext []byte, err error) {
if message == nil && to.Server == types.NewsletterServer {
return
}
plaintext, err = proto.Marshal(message)
if err != nil {
err = fmt.Errorf("failed to marshal message: %w", err)
return
}
if to.Server != types.GroupServer {
if to.Server != types.GroupServer && to.Server != types.NewsletterServer {
dsmPlaintext, err = proto.Marshal(&waProto.Message{
DeviceSentMessage: &waProto.DeviceSentMessage{
DestinationJid: proto.String(to.String()),
@ -929,9 +1031,10 @@ func (cli *Client) encryptMessageForDevice(plaintext []byte, to types.JID, bundl
}
copyAttrs(extraAttrs, encAttrs)
includeDeviceIdentity := encAttrs["type"] == "pkmsg" && cli.MessengerConfig == nil
return &waBinary.Node{
Tag: "enc",
Attrs: encAttrs,
Content: ciphertext.Serialize(),
}, encAttrs["type"] == "pkmsg", nil
}, includeDeviceIdentity, nil
}

606
vendor/go.mau.fi/whatsmeow/sendfb.go vendored Normal file
View File

@ -0,0 +1,606 @@
// Copyright (c) 2024 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"context"
"crypto/hmac"
"crypto/sha256"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"go.mau.fi/libsignal/groups"
"go.mau.fi/libsignal/keys/prekey"
"go.mau.fi/libsignal/protocol"
"go.mau.fi/libsignal/session"
"go.mau.fi/libsignal/signalerror"
"go.mau.fi/util/random"
"google.golang.org/protobuf/proto"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/binary/armadillo/waCommon"
"go.mau.fi/whatsmeow/binary/armadillo/waConsumerApplication"
"go.mau.fi/whatsmeow/binary/armadillo/waMsgApplication"
"go.mau.fi/whatsmeow/binary/armadillo/waMsgTransport"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
const FBMessageVersion = 3
const FBMessageApplicationVersion = 2
const FBConsumerMessageVersion = 1
// SendFBMessage sends the given v3 message to the given JID.
func (cli *Client) SendFBMessage(
ctx context.Context,
to types.JID,
message *waConsumerApplication.ConsumerApplication,
metadata *waMsgApplication.MessageApplication_Metadata,
extra ...SendRequestExtra,
) (resp SendResponse, err error) {
var req SendRequestExtra
if len(extra) > 1 {
err = errors.New("only one extra parameter may be provided to SendMessage")
return
} else if len(extra) == 1 {
req = extra[0]
}
consumerMessage, err := proto.Marshal(message)
if err != nil {
err = fmt.Errorf("failed to marshal consumer message: %w", err)
return
}
if metadata == nil {
metadata = &waMsgApplication.MessageApplication_Metadata{}
}
metadata.FrankingVersion = 0
metadata.FrankingKey = random.Bytes(32)
msgAttrs := getAttrsFromFBMessage(message)
messageAppProto := &waMsgApplication.MessageApplication{
Payload: &waMsgApplication.MessageApplication_Payload{
Content: &waMsgApplication.MessageApplication_Payload_SubProtocol{
SubProtocol: &waMsgApplication.MessageApplication_SubProtocolPayload{
SubProtocol: &waMsgApplication.MessageApplication_SubProtocolPayload_ConsumerMessage{
ConsumerMessage: &waCommon.SubProtocol{
Payload: consumerMessage,
Version: FBConsumerMessageVersion,
},
},
FutureProof: waCommon.FutureProofBehavior_PLACEHOLDER,
},
},
},
Metadata: metadata,
}
messageApp, err := proto.Marshal(messageAppProto)
if err != nil {
return resp, fmt.Errorf("failed to marshal message application: %w", err)
}
frankingHash := hmac.New(sha256.New, metadata.FrankingKey)
frankingHash.Write(messageApp)
frankingTag := frankingHash.Sum(nil)
if to.Device > 0 && !req.Peer {
err = ErrRecipientADJID
return
}
ownID := cli.getOwnID()
if ownID.IsEmpty() {
err = ErrNotLoggedIn
return
}
if req.Timeout == 0 {
req.Timeout = defaultRequestTimeout
}
if len(req.ID) == 0 {
req.ID = cli.GenerateMessageID()
}
resp.ID = req.ID
start := time.Now()
// Sending multiple messages at a time can cause weird issues and makes it harder to retry safely
cli.messageSendLock.Lock()
resp.DebugTimings.Queue = time.Since(start)
defer cli.messageSendLock.Unlock()
respChan := cli.waitResponse(req.ID)
if !req.Peer {
cli.addRecentMessage(to, req.ID, nil, messageAppProto)
}
var phash string
var data []byte
switch to.Server {
case types.GroupServer:
phash, data, err = cli.sendGroupV3(ctx, to, ownID, req.ID, messageApp, msgAttrs, frankingTag, &resp.DebugTimings)
case types.DefaultUserServer, types.MessengerServer:
if req.Peer {
err = fmt.Errorf("peer messages to fb are not yet supported")
//data, err = cli.sendPeerMessage(to, req.ID, message, &resp.DebugTimings)
} else {
data, phash, err = cli.sendDMV3(ctx, to, ownID, req.ID, messageApp, msgAttrs, frankingTag, &resp.DebugTimings)
}
default:
err = fmt.Errorf("%w %s", ErrUnknownServer, to.Server)
}
start = time.Now()
if err != nil {
cli.cancelResponse(req.ID, respChan)
return
}
var respNode *waBinary.Node
var timeoutChan <-chan time.Time
if req.Timeout > 0 {
timeoutChan = time.After(req.Timeout)
} else {
timeoutChan = make(<-chan time.Time)
}
select {
case respNode = <-respChan:
case <-timeoutChan:
cli.cancelResponse(req.ID, respChan)
err = ErrMessageTimedOut
return
case <-ctx.Done():
cli.cancelResponse(req.ID, respChan)
err = ctx.Err()
return
}
resp.DebugTimings.Resp = time.Since(start)
if isDisconnectNode(respNode) {
start = time.Now()
respNode, err = cli.retryFrame("message send", req.ID, data, respNode, ctx, 0)
resp.DebugTimings.Retry = time.Since(start)
if err != nil {
return
}
}
ag := respNode.AttrGetter()
resp.ServerID = types.MessageServerID(ag.OptionalInt("server_id"))
resp.Timestamp = ag.UnixTime("t")
if errorCode := ag.Int("error"); errorCode != 0 {
err = fmt.Errorf("%w %d", ErrServerReturnedError, errorCode)
}
expectedPHash := ag.OptionalString("phash")
if len(expectedPHash) > 0 && phash != expectedPHash {
cli.Log.Warnf("Server returned different participant list hash when sending to %s. Some devices may not have received the message.", to)
// TODO also invalidate device list caches
cli.groupParticipantsCacheLock.Lock()
delete(cli.groupParticipantsCache, to)
cli.groupParticipantsCacheLock.Unlock()
}
return
}
func (cli *Client) sendGroupV3(
ctx context.Context,
to,
ownID types.JID,
id types.MessageID,
messageApp []byte,
msgAttrs messageAttrs,
frankingTag []byte,
timings *MessageDebugTimings,
) (string, []byte, error) {
var participants []types.JID
var err error
start := time.Now()
if to.Server == types.GroupServer {
participants, err = cli.getGroupMembers(ctx, to)
if err != nil {
return "", nil, fmt.Errorf("failed to get group members: %w", err)
}
}
timings.GetParticipants = time.Since(start)
start = time.Now()
builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer)
senderKeyName := protocol.NewSenderKeyName(to.String(), ownID.SignalAddress())
signalSKDMessage, err := builder.Create(senderKeyName)
if err != nil {
return "", nil, fmt.Errorf("failed to create sender key distribution message to send %s to %s: %w", id, to, err)
}
skdm := &waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage{
GroupID: to.String(),
AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(),
}
cipher := groups.NewGroupCipher(builder, senderKeyName, cli.Store)
plaintext, err := proto.Marshal(&waMsgTransport.MessageTransport{
Payload: &waMsgTransport.MessageTransport_Payload{
ApplicationPayload: &waCommon.SubProtocol{
Payload: messageApp,
Version: FBMessageApplicationVersion,
},
FutureProof: waCommon.FutureProofBehavior_PLACEHOLDER,
},
Protocol: &waMsgTransport.MessageTransport_Protocol{
Integral: &waMsgTransport.MessageTransport_Protocol_Integral{
Padding: padMessage(nil),
DSM: nil,
},
Ancillary: &waMsgTransport.MessageTransport_Protocol_Ancillary{
Skdm: nil,
DeviceListMetadata: nil,
Icdc: nil,
BackupDirective: &waMsgTransport.MessageTransport_Protocol_Ancillary_BackupDirective{
MessageID: id,
ActionType: waMsgTransport.MessageTransport_Protocol_Ancillary_BackupDirective_UPSERT,
},
},
},
})
if err != nil {
return "", nil, fmt.Errorf("failed to marshal message transport: %w", err)
}
encrypted, err := cipher.Encrypt(plaintext)
if err != nil {
return "", nil, fmt.Errorf("failed to encrypt group message to send %s to %s: %w", id, to, err)
}
ciphertext := encrypted.SignedSerialize()
timings.GroupEncrypt = time.Since(start)
node, allDevices, err := cli.prepareMessageNodeV3(ctx, to, ownID, id, nil, skdm, msgAttrs, frankingTag, participants, timings)
if err != nil {
return "", nil, err
}
phash := participantListHashV2(allDevices)
node.Attrs["phash"] = phash
skMsg := waBinary.Node{
Tag: "enc",
Content: ciphertext,
Attrs: waBinary.Attrs{"v": "3", "type": "skmsg"},
}
if msgAttrs.MediaType != "" {
skMsg.Attrs["mediatype"] = msgAttrs.MediaType
}
node.Content = append(node.GetChildren(), skMsg)
start = time.Now()
data, err := cli.sendNodeAndGetData(*node)
timings.Send = time.Since(start)
if err != nil {
return "", nil, fmt.Errorf("failed to send message node: %w", err)
}
return phash, data, nil
}
func (cli *Client) sendDMV3(
ctx context.Context,
to,
ownID types.JID,
id types.MessageID,
messageApp []byte,
msgAttrs messageAttrs,
frankingTag []byte,
timings *MessageDebugTimings,
) ([]byte, string, error) {
payload := &waMsgTransport.MessageTransport_Payload{
ApplicationPayload: &waCommon.SubProtocol{
Payload: messageApp,
Version: FBMessageApplicationVersion,
},
FutureProof: waCommon.FutureProofBehavior_PLACEHOLDER,
}
node, allDevices, err := cli.prepareMessageNodeV3(ctx, to, ownID, id, payload, nil, msgAttrs, frankingTag, []types.JID{to, ownID.ToNonAD()}, timings)
if err != nil {
return nil, "", err
}
start := time.Now()
data, err := cli.sendNodeAndGetData(*node)
timings.Send = time.Since(start)
if err != nil {
return nil, "", fmt.Errorf("failed to send message node: %w", err)
}
return data, participantListHashV2(allDevices), nil
}
type messageAttrs struct {
Type string
MediaType string
Edit types.EditAttribute
DecryptFail events.DecryptFailMode
PollType string
}
func getAttrsFromFBMessage(msg *waConsumerApplication.ConsumerApplication) (attrs messageAttrs) {
switch payload := msg.GetPayload().GetPayload().(type) {
case *waConsumerApplication.ConsumerApplication_Payload_Content:
switch content := payload.Content.GetContent().(type) {
case *waConsumerApplication.ConsumerApplication_Content_MessageText,
*waConsumerApplication.ConsumerApplication_Content_ExtendedTextMessage:
attrs.Type = "text"
case *waConsumerApplication.ConsumerApplication_Content_ImageMessage:
attrs.MediaType = "image"
case *waConsumerApplication.ConsumerApplication_Content_StickerMessage:
attrs.MediaType = "sticker"
case *waConsumerApplication.ConsumerApplication_Content_ViewOnceMessage:
switch content.ViewOnceMessage.GetViewOnceContent().(type) {
case *waConsumerApplication.ConsumerApplication_ViewOnceMessage_ImageMessage:
attrs.MediaType = "image"
case *waConsumerApplication.ConsumerApplication_ViewOnceMessage_VideoMessage:
attrs.MediaType = "video"
}
case *waConsumerApplication.ConsumerApplication_Content_DocumentMessage:
attrs.MediaType = "document"
case *waConsumerApplication.ConsumerApplication_Content_AudioMessage:
if content.AudioMessage.GetPTT() {
attrs.MediaType = "ptt"
} else {
attrs.MediaType = "audio"
}
case *waConsumerApplication.ConsumerApplication_Content_VideoMessage:
// TODO gifPlayback?
attrs.MediaType = "video"
case *waConsumerApplication.ConsumerApplication_Content_LocationMessage:
attrs.MediaType = "location"
case *waConsumerApplication.ConsumerApplication_Content_LiveLocationMessage:
attrs.MediaType = "location"
case *waConsumerApplication.ConsumerApplication_Content_ContactMessage:
attrs.MediaType = "vcard"
case *waConsumerApplication.ConsumerApplication_Content_ContactsArrayMessage:
attrs.MediaType = "contact_array"
case *waConsumerApplication.ConsumerApplication_Content_PollCreationMessage:
attrs.PollType = "creation"
attrs.Type = "poll"
case *waConsumerApplication.ConsumerApplication_Content_PollUpdateMessage:
attrs.PollType = "vote"
attrs.Type = "poll"
attrs.DecryptFail = events.DecryptFailHide
case *waConsumerApplication.ConsumerApplication_Content_ReactionMessage:
attrs.Type = "reaction"
attrs.DecryptFail = events.DecryptFailHide
case *waConsumerApplication.ConsumerApplication_Content_EditMessage:
attrs.Edit = types.EditAttributeMessageEdit
attrs.DecryptFail = events.DecryptFailHide
}
if attrs.MediaType != "" && attrs.Type == "" {
attrs.Type = "media"
}
case *waConsumerApplication.ConsumerApplication_Payload_ApplicationData:
switch content := payload.ApplicationData.GetApplicationContent().(type) {
case *waConsumerApplication.ConsumerApplication_ApplicationData_Revoke:
if content.Revoke.GetKey().GetFromMe() {
attrs.Edit = types.EditAttributeSenderRevoke
} else {
attrs.Edit = types.EditAttributeAdminRevoke
}
attrs.DecryptFail = events.DecryptFailHide
}
case *waConsumerApplication.ConsumerApplication_Payload_Signal:
case *waConsumerApplication.ConsumerApplication_Payload_SubProtocol:
}
if attrs.Type == "" {
attrs.Type = "text"
}
return
}
func (cli *Client) prepareMessageNodeV3(
ctx context.Context,
to,
ownID types.JID,
id types.MessageID,
payload *waMsgTransport.MessageTransport_Payload,
skdm *waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage,
msgAttrs messageAttrs,
frankingTag []byte,
participants []types.JID,
timings *MessageDebugTimings,
) (*waBinary.Node, []types.JID, error) {
start := time.Now()
allDevices, err := cli.GetUserDevicesContext(ctx, participants)
timings.GetDevices = time.Since(start)
if err != nil {
return nil, nil, fmt.Errorf("failed to get device list: %w", err)
}
encAttrs := waBinary.Attrs{}
attrs := waBinary.Attrs{
"id": id,
"type": msgAttrs.Type,
"to": to,
}
// Only include mediatype on DMs, for groups it's in the skmsg node
if payload != nil && msgAttrs.MediaType != "" {
encAttrs["mediatype"] = msgAttrs.MediaType
}
if msgAttrs.Edit != "" {
attrs["edit"] = string(msgAttrs.Edit)
}
if msgAttrs.DecryptFail != "" {
encAttrs["decrypt-fail"] = string(msgAttrs.DecryptFail)
}
dsm := &waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage{
DestinationJID: to.String(),
Phash: "",
}
start = time.Now()
participantNodes := cli.encryptMessageForDevicesV3(ctx, allDevices, ownID, id, payload, skdm, dsm, encAttrs)
timings.PeerEncrypt = time.Since(start)
content := make([]waBinary.Node, 0, 4)
content = append(content, waBinary.Node{
Tag: "participants",
Content: participantNodes,
})
metaAttrs := make(waBinary.Attrs)
if msgAttrs.PollType != "" {
metaAttrs["polltype"] = msgAttrs.PollType
}
if msgAttrs.DecryptFail != "" {
metaAttrs["decrypt-fail"] = string(msgAttrs.DecryptFail)
}
if len(metaAttrs) > 0 {
content = append(content, waBinary.Node{
Tag: "meta",
Attrs: metaAttrs,
})
}
traceRequestID := uuid.New()
content = append(content, waBinary.Node{
Tag: "franking",
Content: []waBinary.Node{{
Tag: "franking_tag",
Content: frankingTag,
}},
}, waBinary.Node{
Tag: "trace",
Content: []waBinary.Node{{
Tag: "request_id",
Content: traceRequestID[:],
}},
})
return &waBinary.Node{
Tag: "message",
Attrs: attrs,
Content: content,
}, allDevices, nil
}
func (cli *Client) encryptMessageForDevicesV3(
ctx context.Context,
allDevices []types.JID,
ownID types.JID,
id string,
payload *waMsgTransport.MessageTransport_Payload,
skdm *waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage,
dsm *waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage,
encAttrs waBinary.Attrs,
) []waBinary.Node {
participantNodes := make([]waBinary.Node, 0, len(allDevices))
var retryDevices []types.JID
for _, jid := range allDevices {
var dsmForDevice *waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage
if jid.User == ownID.User {
if jid == ownID {
continue
}
dsmForDevice = dsm
}
encrypted, err := cli.encryptMessageForDeviceAndWrapV3(payload, skdm, dsmForDevice, jid, nil, encAttrs)
if errors.Is(err, ErrNoSession) {
retryDevices = append(retryDevices, jid)
continue
} else if err != nil {
cli.Log.Warnf("Failed to encrypt %s for %s: %v", id, jid, err)
continue
}
participantNodes = append(participantNodes, *encrypted)
}
if len(retryDevices) > 0 {
bundles, err := cli.fetchPreKeys(ctx, retryDevices)
if err != nil {
cli.Log.Warnf("Failed to fetch prekeys for %v to retry encryption: %v", retryDevices, err)
} else {
for _, jid := range retryDevices {
resp := bundles[jid]
if resp.err != nil {
cli.Log.Warnf("Failed to fetch prekey for %s: %v", jid, resp.err)
continue
}
var dsmForDevice *waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage
if jid.User == ownID.User {
dsmForDevice = dsm
}
encrypted, err := cli.encryptMessageForDeviceAndWrapV3(payload, skdm, dsmForDevice, jid, resp.bundle, encAttrs)
if err != nil {
cli.Log.Warnf("Failed to encrypt %s for %s (retry): %v", id, jid, err)
continue
}
participantNodes = append(participantNodes, *encrypted)
}
}
}
return participantNodes
}
func (cli *Client) encryptMessageForDeviceAndWrapV3(
payload *waMsgTransport.MessageTransport_Payload,
skdm *waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage,
dsm *waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage,
to types.JID,
bundle *prekey.Bundle,
encAttrs waBinary.Attrs,
) (*waBinary.Node, error) {
node, err := cli.encryptMessageForDeviceV3(payload, skdm, dsm, to, bundle, encAttrs)
if err != nil {
return nil, err
}
return &waBinary.Node{
Tag: "to",
Attrs: waBinary.Attrs{"jid": to},
Content: []waBinary.Node{*node},
}, nil
}
func (cli *Client) encryptMessageForDeviceV3(
payload *waMsgTransport.MessageTransport_Payload,
skdm *waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage,
dsm *waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage,
to types.JID,
bundle *prekey.Bundle,
extraAttrs waBinary.Attrs,
) (*waBinary.Node, error) {
builder := session.NewBuilderFromSignal(cli.Store, to.SignalAddress(), pbSerializer)
if bundle != nil {
cli.Log.Debugf("Processing prekey bundle for %s", to)
err := builder.ProcessBundle(bundle)
if cli.AutoTrustIdentity && errors.Is(err, signalerror.ErrUntrustedIdentity) {
cli.Log.Warnf("Got %v error while trying to process prekey bundle for %s, clearing stored identity and retrying", err, to)
cli.clearUntrustedIdentity(to)
err = builder.ProcessBundle(bundle)
}
if err != nil {
return nil, fmt.Errorf("failed to process prekey bundle: %w", err)
}
} else if !cli.Store.ContainsSession(to.SignalAddress()) {
return nil, ErrNoSession
}
cipher := session.NewCipher(builder, to.SignalAddress())
plaintext, err := proto.Marshal(&waMsgTransport.MessageTransport{
Payload: payload,
Protocol: &waMsgTransport.MessageTransport_Protocol{
Integral: &waMsgTransport.MessageTransport_Protocol_Integral{
Padding: padMessage(nil),
DSM: dsm,
},
Ancillary: &waMsgTransport.MessageTransport_Protocol_Ancillary{
Skdm: skdm,
DeviceListMetadata: nil,
Icdc: nil,
BackupDirective: nil,
},
},
})
if err != nil {
return nil, fmt.Errorf("failed to marshal message transport: %w", err)
}
ciphertext, err := cipher.Encrypt(plaintext)
if err != nil {
return nil, fmt.Errorf("cipher encryption failed: %w", err)
}
encAttrs := waBinary.Attrs{
"v": FBMessageVersion,
"type": "msg",
}
if ciphertext.Type() == protocol.PREKEY_TYPE {
encAttrs["type"] = "pkmsg"
}
copyAttrs(extraAttrs, encAttrs)
return &waBinary.Node{
Tag: "enc",
Attrs: encAttrs,
Content: ciphertext.Serialize(),
}, nil
}

View File

@ -26,7 +26,7 @@ const (
const (
NoiseStartPattern = "Noise_XX_25519_AESGCM_SHA256\x00\x00\x00\x00"
WAMagicValue = 5
WAMagicValue = 6
)
var WAConnHeader = []byte{'W', 'A', WAMagicValue, token.DictVersion}

View File

@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"sync"
"time"
@ -20,8 +19,6 @@ import (
waLog "go.mau.fi/whatsmeow/util/log"
)
type Proxy = func(*http.Request) (*url.URL, error)
type FrameSocket struct {
conn *websocket.Conn
ctx context.Context
@ -29,12 +26,15 @@ type FrameSocket struct {
log waLog.Logger
lock sync.Mutex
URL string
HTTPHeaders http.Header
Frames chan []byte
OnDisconnect func(remote bool)
WriteTimeout time.Duration
Header []byte
Proxy Proxy
Dialer websocket.Dialer
incomingLength int
receivedLength int
@ -42,14 +42,17 @@ type FrameSocket struct {
partialHeader []byte
}
func NewFrameSocket(log waLog.Logger, header []byte, proxy Proxy) *FrameSocket {
func NewFrameSocket(log waLog.Logger, dialer websocket.Dialer) *FrameSocket {
return &FrameSocket{
conn: nil,
log: log,
Header: header,
Header: WAConnHeader,
Frames: make(chan []byte),
Proxy: proxy,
URL: URL,
HTTPHeaders: http.Header{"Origin": {Origin}},
Dialer: dialer,
}
}
@ -98,13 +101,9 @@ func (fs *FrameSocket) Connect() error {
return ErrSocketAlreadyOpen
}
ctx, cancel := context.WithCancel(context.Background())
dialer := websocket.Dialer{
Proxy: fs.Proxy,
}
headers := http.Header{"Origin": []string{Origin}}
fs.log.Debugf("Dialing %s", URL)
conn, _, err := dialer.Dial(URL, headers)
fs.log.Debugf("Dialing %s", fs.URL)
conn, _, err := fs.Dialer.Dial(fs.URL, fs.HTTPHeaders)
if err != nil {
cancel()
return fmt.Errorf("couldn't dial whatsapp web websocket: %w", err)

View File

@ -24,7 +24,7 @@ type NoiseSocket struct {
writeCounter uint32
readCounter uint32
writeLock sync.Mutex
destroyed uint32
destroyed atomic.Bool
stopConsumer chan struct{}
}
@ -75,7 +75,7 @@ func (ns *NoiseSocket) Context() context.Context {
}
func (ns *NoiseSocket) Stop(disconnect bool) {
if atomic.CompareAndSwapUint32(&ns.destroyed, 0, 1) {
if ns.destroyed.CompareAndSwap(false, true) {
close(ns.stopConsumer)
ns.fs.OnDisconnect = nil
if disconnect {

View File

@ -74,7 +74,7 @@ func (vc WAVersionContainer) ProtoAppVersion() *waProto.ClientPayload_UserAgent_
}
// waVersion is the WhatsApp web client version
var waVersion = WAVersionContainer{2, 2332, 15}
var waVersion = WAVersionContainer{2, 2412, 50}
// waVersionHash is the md5 hash of a dot-separated waVersion
var waVersionHash [16]byte

View File

@ -12,12 +12,14 @@ import (
"fmt"
mathRand "math/rand"
"github.com/google/uuid"
"go.mau.fi/util/random"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/keys"
waLog "go.mau.fi/whatsmeow/util/log"
"go.mau.fi/whatsmeow/util/randbytes"
)
// Container is a wrapper for a SQL database that can contain multiple whatsmeow sessions.
@ -86,7 +88,7 @@ const getAllDevicesQuery = `
SELECT jid, registration_id, noise_key, identity_key,
signed_pre_key, signed_pre_key_id, signed_pre_key_sig,
adv_key, adv_details, adv_account_sig, adv_account_sig_key, adv_device_sig,
platform, business_name, push_name
platform, business_name, push_name, facebook_uuid
FROM whatsmeow_device
`
@ -103,12 +105,13 @@ func (c *Container) scanDevice(row scannable) (*store.Device, error) {
device.SignedPreKey = &keys.PreKey{}
var noisePriv, identityPriv, preKeyPriv, preKeySig []byte
var account waProto.ADVSignedDeviceIdentity
var fbUUID uuid.NullUUID
err := row.Scan(
&device.ID, &device.RegistrationID, &noisePriv, &identityPriv,
&preKeyPriv, &device.SignedPreKey.KeyID, &preKeySig,
&device.AdvSecretKey, &account.Details, &account.AccountSignature, &account.AccountSignatureKey, &account.DeviceSignature,
&device.Platform, &device.BusinessName, &device.PushName)
&device.Platform, &device.BusinessName, &device.PushName, &fbUUID)
if err != nil {
return nil, fmt.Errorf("failed to scan session: %w", err)
} else if len(noisePriv) != 32 || len(identityPriv) != 32 || len(preKeyPriv) != 32 || len(preKeySig) != 64 {
@ -120,6 +123,7 @@ func (c *Container) scanDevice(row scannable) (*store.Device, error) {
device.SignedPreKey.KeyPair = *keys.NewKeyPairFromPrivateKey(*(*[32]byte)(preKeyPriv))
device.SignedPreKey.Signature = (*[64]byte)(preKeySig)
device.Account = &account
device.FacebookUUID = fbUUID.UUID
innerStore := NewSQLStore(c, *device.ID)
device.Identities = innerStore
@ -188,8 +192,8 @@ const (
INSERT INTO whatsmeow_device (jid, registration_id, noise_key, identity_key,
signed_pre_key, signed_pre_key_id, signed_pre_key_sig,
adv_key, adv_details, adv_account_sig, adv_account_sig_key, adv_device_sig,
platform, business_name, push_name)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
platform, business_name, push_name, facebook_uuid)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
ON CONFLICT (jid) DO UPDATE
SET platform=excluded.platform, business_name=excluded.business_name, push_name=excluded.push_name
`
@ -210,7 +214,7 @@ func (c *Container) NewDevice() *store.Device {
NoiseKey: keys.NewKeyPair(),
IdentityKey: keys.NewKeyPair(),
RegistrationID: mathRand.Uint32(),
AdvSecretKey: randbytes.Make(32),
AdvSecretKey: random.Bytes(32),
}
device.SignedPreKey = device.IdentityKey.CreateSignedPreKey(1)
return device
@ -219,6 +223,14 @@ func (c *Container) NewDevice() *store.Device {
// ErrDeviceIDMustBeSet is the error returned by PutDevice if you try to save a device before knowing its JID.
var ErrDeviceIDMustBeSet = errors.New("device JID must be known before accessing database")
// Close will close the container's database
func (c *Container) Close() error {
if c != nil && c.db != nil {
return c.db.Close()
}
return nil
}
// PutDevice stores the given device in this database. This should be called through Device.Save()
// (which usually doesn't need to be called manually, as the library does that automatically when relevant).
func (c *Container) PutDevice(device *store.Device) error {
@ -229,7 +241,7 @@ func (c *Container) PutDevice(device *store.Device) error {
device.ID.String(), device.RegistrationID, device.NoiseKey.Priv[:], device.IdentityKey.Priv[:],
device.SignedPreKey.Priv[:], device.SignedPreKey.KeyID, device.SignedPreKey.Signature[:],
device.AdvSecretKey, device.Account.Details, device.Account.AccountSignature, device.Account.AccountSignatureKey, device.Account.DeviceSignature,
device.Platform, device.BusinessName, device.PushName)
device.Platform, device.BusinessName, device.PushName, uuid.NullUUID{UUID: device.FacebookUUID, Valid: device.FacebookUUID != uuid.Nil})
if !device.Initialized {
innerStore := NewSQLStore(c, *device.ID)

View File

@ -8,6 +8,7 @@ package sqlstore
import (
"database/sql"
"fmt"
)
type upgradeFunc func(*sql.Tx, *Container) error
@ -16,7 +17,7 @@ type upgradeFunc func(*sql.Tx, *Container) error
//
// This may be of use if you want to manage the database fully manually, but in most cases you
// should just call Container.Upgrade to let the library handle everything.
var Upgrades = [...]upgradeFunc{upgradeV1, upgradeV2, upgradeV3, upgradeV4}
var Upgrades = [...]upgradeFunc{upgradeV1, upgradeV2, upgradeV3, upgradeV4, upgradeV5, upgradeV6}
func (c *Container) getVersion() (int, error) {
_, err := c.db.Exec("CREATE TABLE IF NOT EXISTS whatsmeow_version (version INTEGER)")
@ -43,6 +44,16 @@ func (c *Container) setVersion(tx *sql.Tx, version int) error {
// Upgrade upgrades the database from the current to the latest version available.
func (c *Container) Upgrade() error {
if c.dialect == "sqlite" {
var foreignKeysEnabled bool
err := c.db.QueryRow("PRAGMA foreign_keys").Scan(&foreignKeysEnabled)
if err != nil {
return fmt.Errorf("failed to check if foreign keys are enabled: %w", err)
} else if !foreignKeysEnabled {
return fmt.Errorf("foreign keys are not enabled")
}
}
version, err := c.getVersion()
if err != nil {
return err
@ -271,3 +282,13 @@ func upgradeV4(tx *sql.Tx, container *Container) error {
)`)
return err
}
func upgradeV5(tx *sql.Tx, container *Container) error {
_, err := tx.Exec("UPDATE whatsmeow_device SET jid=REPLACE(jid, '.0', '')")
return err
}
func upgradeV6(tx *sql.Tx, container *Container) error {
_, err := tx.Exec("ALTER TABLE whatsmeow_device ADD COLUMN facebook_uuid uuid")
return err
}

View File

@ -11,6 +11,8 @@ import (
"fmt"
"time"
"github.com/google/uuid"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/keys"
@ -139,6 +141,8 @@ type Device struct {
BusinessName string
PushName string
FacebookUUID uuid.UUID
Initialized bool
Identities IdentityStore
Sessions SessionStore

View File

@ -142,6 +142,36 @@ type UserStatusMute struct {
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// LabelEdit is emitted when a label is edited from any device.
type LabelEdit struct {
Timestamp time.Time // The time when the label was edited.
LabelID string // The label id which was edited.
Action *waProto.LabelEditAction // The new label info.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// LabelAssociationChat is emitted when a chat is labeled or unlabeled from any device.
type LabelAssociationChat struct {
JID types.JID // The chat which was labeled or unlabeled.
Timestamp time.Time // The time when the (un)labeling happened.
LabelID string // The label id which was added or removed.
Action *waProto.LabelAssociationAction // The current label status of the chat.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// LabelAssociationMessage is emitted when a message is labeled or unlabeled from any device.
type LabelAssociationMessage struct {
JID types.JID // The chat which was labeled or unlabeled.
Timestamp time.Time // The time when the (un)labeling happened.
LabelID string // The label id which was added or removed.
MessageID string // The message id which was labeled or unlabeled.
Action *waProto.LabelAssociationAction // The current label status of the message.
FromFullSync bool // Whether the action is emitted because of a fullSync
}
// AppState is emitted directly for new data received from app state syncing.
// You should generally use the higher-level events like events.Contact and events.Mute.
type AppState struct {

View File

@ -27,6 +27,20 @@ type CallAccept struct {
Data *waBinary.Node
}
type CallPreAccept struct {
types.BasicCallMeta
types.CallRemoteMeta
Data *waBinary.Node
}
type CallTransport struct {
types.BasicCallMeta
types.CallRemoteMeta
Data *waBinary.Node
}
// CallOfferNotice is emitted when the user receives a notice of a call on WhatsApp.
// This seems to be primarily for group calls (whereas CallOffer is for 1:1 calls).
type CallOfferNotice struct {

View File

@ -9,9 +9,13 @@ package events
import (
"fmt"
"strconv"
"time"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/binary/armadillo"
"go.mau.fi/whatsmeow/binary/armadillo/waMsgApplication"
"go.mau.fi/whatsmeow/binary/armadillo/waMsgTransport"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
)
@ -69,6 +73,22 @@ type KeepAliveTimeout struct {
// Note that if the websocket disconnects before the pings start working, this event will not be emitted.
type KeepAliveRestored struct{}
// PermanentDisconnect is a class of events emitted when the client will not auto-reconnect by default.
type PermanentDisconnect interface {
PermanentDisconnectDescription() string
}
func (l *LoggedOut) PermanentDisconnectDescription() string { return l.Reason.String() }
func (*StreamReplaced) PermanentDisconnectDescription() string { return "stream replaced" }
func (*ClientOutdated) PermanentDisconnectDescription() string { return "client outdated" }
func (*CATRefreshError) PermanentDisconnectDescription() string { return "CAT refresh failed" }
func (tb *TemporaryBan) PermanentDisconnectDescription() string {
return fmt.Sprintf("temporarily banned: %s", tb.String())
}
func (cf *ConnectFailure) PermanentDisconnectDescription() string {
return fmt.Sprintf("connect failure: %s", cf.Reason.String())
}
// LoggedOut is emitted when the client has been unpaired from the phone.
//
// This can happen while connected (stream:error messages) or right after connecting (connect failure messages).
@ -133,20 +153,22 @@ func (tb *TemporaryBan) String() string {
type ConnectFailureReason int
const (
ConnectFailureGeneric ConnectFailureReason = 400
ConnectFailureLoggedOut ConnectFailureReason = 401
ConnectFailureTempBanned ConnectFailureReason = 402
ConnectFailureMainDeviceGone ConnectFailureReason = 403
ConnectFailureUnknownLogout ConnectFailureReason = 406
ConnectFailureMainDeviceGone ConnectFailureReason = 403 // this is now called LOCKED in the whatsapp web code
ConnectFailureUnknownLogout ConnectFailureReason = 406 // this is now called BANNED in the whatsapp web code
ConnectFailureClientOutdated ConnectFailureReason = 405
ConnectFailureBadUserAgent ConnectFailureReason = 409
// 400, 500 and 501 are also existing codes, but the meaning is unknown
ConnectFailureCATExpired ConnectFailureReason = 413
ConnectFailureCATInvalid ConnectFailureReason = 414
ConnectFailureNotFound ConnectFailureReason = 415
// 503 doesn't seem to be included in the web app JS with the other codes, and it's very rare,
// but does happen after a 503 stream error sometimes.
ConnectFailureServiceUnavailable ConnectFailureReason = 503
ConnectFailureInternalServerError ConnectFailureReason = 500
ConnectFailureExperimental ConnectFailureReason = 501
ConnectFailureServiceUnavailable ConnectFailureReason = 503
)
var connectFailureReasonMessage = map[ConnectFailureReason]string{
@ -156,6 +178,8 @@ var connectFailureReasonMessage = map[ConnectFailureReason]string{
ConnectFailureUnknownLogout: "logged out for unknown reason",
ConnectFailureClientOutdated: "client is out of date",
ConnectFailureBadUserAgent: "client user agent was rejected",
ConnectFailureCATExpired: "messenger crypto auth token has expired",
ConnectFailureCATInvalid: "messenger crypto auth token is invalid",
}
// IsLoggedOut returns true if the client should delete session data due to this connect failure.
@ -163,6 +187,10 @@ func (cfr ConnectFailureReason) IsLoggedOut() bool {
return cfr == ConnectFailureLoggedOut || cfr == ConnectFailureMainDeviceGone || cfr == ConnectFailureUnknownLogout
}
func (cfr ConnectFailureReason) NumberString() string {
return strconv.Itoa(int(cfr))
}
// String returns the reason code and a short human-readable description of the error.
func (cfr ConnectFailureReason) String() string {
msg, ok := connectFailureReasonMessage[cfr]
@ -184,6 +212,10 @@ type ConnectFailure struct {
// ClientOutdated is emitted when the WhatsApp server rejects the connection with the ConnectFailureClientOutdated code.
type ClientOutdated struct{}
type CATRefreshError struct {
Error error
}
// StreamError is emitted when the WhatsApp server sends a <stream:error> node with an unknown code.
//
// Known codes are handled internally and emitted as different events (e.g. LoggedOut).
@ -223,6 +255,14 @@ type UndecryptableMessage struct {
DecryptFailMode DecryptFailMode
}
type NewsletterMessageMeta struct {
// When a newsletter message is edited, the message isn't wrapped in an EditedMessage like normal messages.
// Instead, the message is the new content, the ID is the original message ID, and the edit timestamp is here.
EditTS time.Time
// This is the timestamp of the original message for edits.
OriginalTS time.Time
}
// Message is emitted when receiving a new message.
type Message struct {
Info types.MessageInfo // Information about the message like the chat and sender IDs
@ -241,11 +281,24 @@ type Message struct {
// If the message was re-requested from the sender, this is the number of retries it took.
RetryCount int
NewsletterMeta *NewsletterMessageMeta
// The raw message struct. This is the raw unmodified data, which means the actual message might
// be wrapped in DeviceSentMessage, EphemeralMessage or ViewOnceMessage.
RawMessage *waProto.Message
}
type FBMessage struct {
Info types.MessageInfo // Information about the message like the chat and sender IDs
Message armadillo.MessageApplicationSub // The actual message struct
// If the message was re-requested from the sender, this is the number of retries it took.
RetryCount int
Transport *waMsgTransport.MessageTransport // The first level of wrapping the message was in
Application *waMsgApplication.MessageApplication // The second level of wrapping the message was in
}
// UnwrapRaw fills the Message, IsEphemeral and IsViewOnce fields based on the raw message in the RawMessage field.
func (evt *Message) UnwrapRaw() *Message {
evt.Message = evt.RawMessage
@ -280,44 +333,19 @@ func (evt *Message) UnwrapRaw() *Message {
return evt
}
// ReceiptType represents the type of a Receipt event.
type ReceiptType string
// Deprecated: use types.ReceiptType directly
type ReceiptType = types.ReceiptType
// Deprecated: use types.ReceiptType* constants directly
const (
// ReceiptTypeDelivered means the message was delivered to the device (but the user might not have noticed).
ReceiptTypeDelivered ReceiptType = ""
// ReceiptTypeSender is sent by your other devices when a message you sent is delivered to them.
ReceiptTypeSender ReceiptType = "sender"
// ReceiptTypeRetry means the message was delivered to the device, but decrypting the message failed.
ReceiptTypeRetry ReceiptType = "retry"
// ReceiptTypeRead means the user opened the chat and saw the message.
ReceiptTypeRead ReceiptType = "read"
// ReceiptTypeReadSelf means the current user read a message from a different device, and has read receipts disabled in privacy settings.
ReceiptTypeReadSelf ReceiptType = "read-self"
// ReceiptTypePlayed means the user opened a view-once media message.
//
// This is dispatched for both incoming and outgoing messages when played. If the current user opened the media,
// it means the media should be removed from all devices. If a recipient opened the media, it's just a notification
// for the sender that the media was viewed.
ReceiptTypePlayed ReceiptType = "played"
ReceiptTypeDelivered = types.ReceiptTypeDelivered
ReceiptTypeSender = types.ReceiptTypeSender
ReceiptTypeRetry = types.ReceiptTypeRetry
ReceiptTypeRead = types.ReceiptTypeRead
ReceiptTypeReadSelf = types.ReceiptTypeReadSelf
ReceiptTypePlayed = types.ReceiptTypePlayed
)
// GoString returns the name of the Go constant for the ReceiptType value.
func (rt ReceiptType) GoString() string {
switch rt {
case ReceiptTypeRead:
return "events.ReceiptTypeRead"
case ReceiptTypeReadSelf:
return "events.ReceiptTypeReadSelf"
case ReceiptTypeDelivered:
return "events.ReceiptTypeDelivered"
case ReceiptTypePlayed:
return "events.ReceiptTypePlayed"
default:
return fmt.Sprintf("events.ReceiptType(%#v)", string(rt))
}
}
// Receipt is emitted when an outgoing message is delivered to or read by another user, or when another device reads an incoming message.
//
// N.B. WhatsApp on Android sends message IDs from newest message to oldest, but WhatsApp on iOS sends them in the opposite order (oldest first).
@ -325,7 +353,7 @@ type Receipt struct {
types.MessageSource
MessageIDs []types.MessageID
Timestamp time.Time
Type ReceiptType
Type types.ReceiptType
}
// ChatPresence is emitted when a chat state update (also known as typing notification) is received.
@ -424,6 +452,8 @@ type PrivacySettings struct {
StatusChanged bool
ProfileChanged bool
ReadReceiptsChanged bool
OnlineChanged bool
CallAddChanged bool
}
// OfflineSyncPreview is emitted right after connecting if the server is going to send events that the client missed during downtime.
@ -460,3 +490,52 @@ type MediaRetry struct {
SenderID types.JID // The user who sent the message. Only present in groups.
FromMe bool // Whether the message was sent by the current user or someone else.
}
type BlocklistAction string
const (
BlocklistActionDefault BlocklistAction = ""
BlocklistActionModify BlocklistAction = "modify"
)
// Blocklist is emitted when the user's blocked user list is changed.
type Blocklist struct {
// Action specifies what happened. If it's empty, there should be a list of changes in the Changes list.
// If it's "modify", then the Changes list will be empty and the whole blocklist should be re-requested.
Action BlocklistAction
DHash string
PrevDHash string
Changes []BlocklistChange
}
type BlocklistChangeAction string
const (
BlocklistChangeActionBlock BlocklistChangeAction = "block"
BlocklistChangeActionUnblock BlocklistChangeAction = "unblock"
)
type BlocklistChange struct {
JID types.JID
Action BlocklistChangeAction
}
type NewsletterJoin struct {
types.NewsletterMetadata
}
type NewsletterLeave struct {
ID types.JID `json:"id"`
Role types.NewsletterRole `json:"role"`
}
type NewsletterMuteChange struct {
ID types.JID `json:"id"`
Mute types.NewsletterMuteState `json:"mute"`
}
type NewsletterLiveUpdate struct {
JID types.JID
Time time.Time
Messages []*types.NewsletterMessage
}

View File

@ -26,6 +26,7 @@ type GroupInfo struct {
GroupLocked
GroupAnnounce
GroupEphemeral
GroupIncognito
GroupParent
GroupLinkedParent
@ -79,12 +80,20 @@ type GroupAnnounce struct {
AnnounceVersionID string
}
type GroupIncognito struct {
IsIncognito bool
}
// GroupParticipant contains info about a participant of a WhatsApp group chat.
type GroupParticipant struct {
JID JID
LID JID
IsAdmin bool
IsSuperAdmin bool
// This is only present for anonymous users in announcement groups, it's an obfuscated phone number
DisplayName string
// When creating groups, adding some participants may fail.
// In such cases, the error code will be here.
Error int

View File

@ -24,6 +24,10 @@ const (
LegacyUserServer = "c.us"
BroadcastServer = "broadcast"
HiddenUserServer = "lid"
MessengerServer = "msgr"
InteropServer = "interop"
NewsletterServer = "newsletter"
HostedServer = "hosted"
)
// Some JIDs that are contacted often.
@ -40,17 +44,31 @@ var (
// MessageID is the internal ID of a WhatsApp message.
type MessageID = string
// MessageServerID is the server ID of a WhatsApp newsletter message.
type MessageServerID = int
// JID represents a WhatsApp user ID.
//
// There are two types of JIDs: regular JID pairs (user and server) and AD-JIDs (user, agent and device).
// AD JIDs are only used to refer to specific devices of users, so the server is always s.whatsapp.net (DefaultUserServer).
// Regular JIDs can be used for entities on any servers (users, groups, broadcasts).
type JID struct {
User string
Agent uint8
Device uint8
Server string
AD bool
User string
RawAgent uint8
Device uint16
Integrator uint16
Server string
}
func (jid JID) ActualAgent() uint8 {
switch jid.Server {
case DefaultUserServer:
return 0
case HiddenUserServer:
return 1
default:
return jid.RawAgent
}
}
// UserInt returns the user as an integer. This is only safe to run on normal users, not on groups or broadcast lists.
@ -61,23 +79,27 @@ func (jid JID) UserInt() uint64 {
// ToNonAD returns a version of the JID struct that doesn't have the agent and device set.
func (jid JID) ToNonAD() JID {
if jid.AD {
return JID{
User: jid.User,
Server: DefaultUserServer,
}
} else {
return jid
return JID{
User: jid.User,
Server: jid.Server,
Integrator: jid.Integrator,
}
}
// SignalAddress returns the Signal protocol address for the user.
func (jid JID) SignalAddress() *signalProtocol.SignalAddress {
user := jid.User
if jid.Agent != 0 {
user = fmt.Sprintf("%s_%d", jid.User, jid.Agent)
agent := jid.ActualAgent()
if agent != 0 {
user = fmt.Sprintf("%s_%d", jid.User, agent)
}
return signalProtocol.NewSignalAddress(user, uint32(jid.Device))
// TODO use @lid suffix instead of agent?
//suffix := ""
//if jid.Server == HiddenUserServer {
// suffix = "@lid"
//}
//return signalProtocol.NewSignalAddress(user, uint32(jid.Device), suffix)
}
// IsBroadcastList returns true if the JID is a broadcast list, but not the status broadcast.
@ -87,53 +109,70 @@ func (jid JID) IsBroadcastList() bool {
// NewADJID creates a new AD JID.
func NewADJID(user string, agent, device uint8) JID {
var server string
switch agent {
case 0:
server = DefaultUserServer
case 1:
server = HiddenUserServer
agent = 0
default:
if (agent&0x01) != 0 || (agent&0x80) == 0 { // agent % 2 == 0 || agent < 128?
// TODO invalid JID?
}
server = HostedServer
}
return JID{
User: user,
Agent: agent,
Device: device,
Server: DefaultUserServer,
AD: true,
User: user,
RawAgent: agent,
Device: uint16(device),
Server: server,
}
}
func parseADJID(user string) (JID, error) {
var fullJID JID
fullJID.AD = true
fullJID.Server = DefaultUserServer
dotIndex := strings.IndexRune(user, '.')
colonIndex := strings.IndexRune(user, ':')
if dotIndex < 0 || colonIndex < 0 || colonIndex+1 <= dotIndex {
return fullJID, fmt.Errorf("failed to parse ADJID: missing separators")
}
fullJID.User = user[:dotIndex]
agent, err := strconv.Atoi(user[dotIndex+1 : colonIndex])
if err != nil {
return fullJID, fmt.Errorf("failed to parse agent from JID: %w", err)
} else if agent < 0 || agent > 255 {
return fullJID, fmt.Errorf("failed to parse agent from JID: invalid value (%d)", agent)
}
device, err := strconv.Atoi(user[colonIndex+1:])
if err != nil {
return fullJID, fmt.Errorf("failed to parse device from JID: %w", err)
} else if device < 0 || device > 255 {
return fullJID, fmt.Errorf("failed to parse device from JID: invalid value (%d)", device)
}
fullJID.Agent = uint8(agent)
fullJID.Device = uint8(device)
return fullJID, nil
}
// ParseJID parses a JID out of the given string. It supports both regular and AD JIDs.
func ParseJID(jid string) (JID, error) {
parts := strings.Split(jid, "@")
if len(parts) == 1 {
return NewJID("", parts[0]), nil
} else if strings.ContainsRune(parts[0], ':') && strings.ContainsRune(parts[0], '.') && parts[1] == DefaultUserServer {
return parseADJID(parts[0])
}
return NewJID(parts[0], parts[1]), nil
parsedJID := JID{User: parts[0], Server: parts[1]}
if strings.ContainsRune(parsedJID.User, '.') {
parts = strings.Split(parsedJID.User, ".")
if len(parts) != 2 {
return parsedJID, fmt.Errorf("unexpected number of dots in JID")
}
parsedJID.User = parts[0]
ad := parts[1]
parts = strings.Split(ad, ":")
if len(parts) > 2 {
return parsedJID, fmt.Errorf("unexpected number of colons in JID")
}
agent, err := strconv.Atoi(parts[0])
if err != nil {
return parsedJID, fmt.Errorf("failed to parse device from JID: %w", err)
}
parsedJID.RawAgent = uint8(agent)
if len(parts) == 2 {
device, err := strconv.Atoi(parts[1])
if err != nil {
return parsedJID, fmt.Errorf("failed to parse device from JID: %w", err)
}
parsedJID.Device = uint16(device)
}
} else if strings.ContainsRune(parsedJID.User, ':') {
parts = strings.Split(parsedJID.User, ":")
if len(parts) != 2 {
return parsedJID, fmt.Errorf("unexpected number of colons in JID")
}
parsedJID.User = parts[0]
device, err := strconv.Atoi(parts[1])
if err != nil {
return parsedJID, fmt.Errorf("failed to parse device from JID: %w", err)
}
parsedJID.Device = uint16(device)
}
return parsedJID, nil
}
// NewJID creates a new regular JID.
@ -144,11 +183,17 @@ func NewJID(user, server string) JID {
}
}
func (jid JID) ADString() string {
return fmt.Sprintf("%s.%d:%d@%s", jid.User, jid.RawAgent, jid.Device, jid.Server)
}
// String converts the JID to a string representation.
// The output string can be parsed with ParseJID, except for JIDs with no User part specified.
// The output string can be parsed with ParseJID.
func (jid JID) String() string {
if jid.AD {
return fmt.Sprintf("%s.%d:%d@%s", jid.User, jid.Agent, jid.Device, jid.Server)
if jid.RawAgent > 0 {
return fmt.Sprintf("%s.%d:%d@%s", jid.User, jid.RawAgent, jid.Device, jid.Server)
} else if jid.Device > 0 {
return fmt.Sprintf("%s:%d@%s", jid.User, jid.Device, jid.Server)
} else if len(jid.User) > 0 {
return fmt.Sprintf("%s@%s", jid.User, jid.Server)
} else {

View File

@ -36,16 +36,29 @@ type DeviceSentMeta struct {
Phash string
}
type EditAttribute string
const (
EditAttributeEmpty EditAttribute = ""
EditAttributeMessageEdit EditAttribute = "1"
EditAttributePinInChat EditAttribute = "2"
EditAttributeAdminEdit EditAttribute = "3" // only used in newsletters
EditAttributeSenderRevoke EditAttribute = "7"
EditAttributeAdminRevoke EditAttribute = "8"
)
// MessageInfo contains metadata about an incoming message.
type MessageInfo struct {
MessageSource
ID string
ID MessageID
ServerID MessageServerID
Type string
PushName string
Timestamp time.Time
Category string
Multicast bool
MediaType string
Edit EditAttribute
VerifiedName *VerifiedName
DeviceSentMeta *DeviceSentMeta // Metadata for direct messages sent from another one of the user's own devices.

View File

@ -0,0 +1,197 @@
// Copyright (c) 2023 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package types
import (
"bytes"
"encoding/json"
"fmt"
"go.mau.fi/util/jsontime"
waProto "go.mau.fi/whatsmeow/binary/proto"
)
type NewsletterVerificationState string
func (nvs *NewsletterVerificationState) UnmarshalText(text []byte) error {
*nvs = NewsletterVerificationState(bytes.ToLower(text))
return nil
}
const (
NewsletterVerificationStateVerified NewsletterVerificationState = "verified"
NewsletterVerificationStateUnverified NewsletterVerificationState = "unverified"
)
type NewsletterPrivacy string
func (np *NewsletterPrivacy) UnmarshalText(text []byte) error {
*np = NewsletterPrivacy(bytes.ToLower(text))
return nil
}
const (
NewsletterPrivacyPrivate NewsletterPrivacy = "private"
NewsletterPrivacyPublic NewsletterPrivacy = "public"
)
type NewsletterReactionsMode string
const (
NewsletterReactionsModeAll NewsletterReactionsMode = "all"
NewsletterReactionsModeBasic NewsletterReactionsMode = "basic"
NewsletterReactionsModeNone NewsletterReactionsMode = "none"
NewsletterReactionsModeBlocklist NewsletterReactionsMode = "blocklist"
)
type NewsletterState string
func (ns *NewsletterState) UnmarshalText(text []byte) error {
*ns = NewsletterState(bytes.ToLower(text))
return nil
}
const (
NewsletterStateActive NewsletterState = "active"
NewsletterStateSuspended NewsletterState = "suspended"
NewsletterStateGeoSuspended NewsletterState = "geosuspended"
)
type NewsletterMuted struct {
Muted bool
}
type WrappedNewsletterState struct {
Type NewsletterState `json:"type"`
}
type NewsletterMuteState string
func (nms *NewsletterMuteState) UnmarshalText(text []byte) error {
*nms = NewsletterMuteState(bytes.ToLower(text))
return nil
}
const (
NewsletterMuteOn NewsletterMuteState = "on"
NewsletterMuteOff NewsletterMuteState = "off"
)
type NewsletterRole string
func (nr *NewsletterRole) UnmarshalText(text []byte) error {
*nr = NewsletterRole(bytes.ToLower(text))
return nil
}
const (
NewsletterRoleSubscriber NewsletterRole = "subscriber"
NewsletterRoleGuest NewsletterRole = "guest"
NewsletterRoleAdmin NewsletterRole = "admin"
NewsletterRoleOwner NewsletterRole = "owner"
)
type NewsletterMetadata struct {
ID JID `json:"id"`
State WrappedNewsletterState `json:"state"`
ThreadMeta NewsletterThreadMetadata `json:"thread_metadata"`
ViewerMeta *NewsletterViewerMetadata `json:"viewer_metadata"`
}
type NewsletterViewerMetadata struct {
Mute NewsletterMuteState `json:"mute"`
Role NewsletterRole `json:"role"`
}
type NewsletterKeyType string
const (
NewsletterKeyTypeJID NewsletterKeyType = "JID"
NewsletterKeyTypeInvite NewsletterKeyType = "INVITE"
)
type NewsletterReactionSettings struct {
Value NewsletterReactionsMode `json:"value"`
}
type NewsletterSettings struct {
ReactionCodes NewsletterReactionSettings `json:"reaction_codes"`
}
type NewsletterThreadMetadata struct {
CreationTime jsontime.UnixString `json:"creation_time"`
InviteCode string `json:"invite"`
Name NewsletterText `json:"name"`
Description NewsletterText `json:"description"`
SubscriberCount int `json:"subscribers_count,string"`
VerificationState NewsletterVerificationState `json:"verification"`
Picture *ProfilePictureInfo `json:"picture"`
Preview ProfilePictureInfo `json:"preview"`
Settings NewsletterSettings `json:"settings"`
//NewsletterMuted `json:"-"`
//PrivacyType NewsletterPrivacy `json:"-"`
//ReactionsMode NewsletterReactionsMode `json:"-"`
//State NewsletterState `json:"-"`
}
type NewsletterText struct {
Text string `json:"text"`
ID string `json:"id"`
UpdateTime jsontime.UnixMicroString `json:"update_time"`
}
type NewsletterMessage struct {
MessageServerID MessageServerID
ViewsCount int
ReactionCounts map[string]int
// This is only present when fetching messages, not in live updates
Message *waProto.Message
}
type GraphQLErrorExtensions struct {
ErrorCode int `json:"error_code"`
IsRetryable bool `json:"is_retryable"`
Severity string `json:"severity"`
}
type GraphQLError struct {
Extensions GraphQLErrorExtensions `json:"extensions"`
Message string `json:"message"`
Path []string `json:"path"`
}
func (gqle GraphQLError) Error() string {
return fmt.Sprintf("%d %s (%s)", gqle.Extensions.ErrorCode, gqle.Message, gqle.Extensions.Severity)
}
type GraphQLErrors []GraphQLError
func (gqles GraphQLErrors) Unwrap() []error {
errs := make([]error, len(gqles))
for i, gqle := range gqles {
errs[i] = gqle
}
return errs
}
func (gqles GraphQLErrors) Error() string {
if len(gqles) == 0 {
return ""
} else if len(gqles) == 1 {
return gqles[0].Error()
} else {
return fmt.Sprintf("%v (and %d other errors)", gqles[0], len(gqles)-1)
}
}
type GraphQLResponse struct {
Data json.RawMessage `json:"data"`
Errors GraphQLErrors `json:"errors"`
}

View File

@ -6,6 +6,10 @@
package types
import (
"fmt"
)
type Presence string
const (
@ -26,3 +30,49 @@ const (
ChatPresenceMediaText ChatPresenceMedia = ""
ChatPresenceMediaAudio ChatPresenceMedia = "audio"
)
// ReceiptType represents the type of a Receipt event.
type ReceiptType string
const (
// ReceiptTypeDelivered means the message was delivered to the device (but the user might not have noticed).
ReceiptTypeDelivered ReceiptType = ""
// ReceiptTypeSender is sent by your other devices when a message you sent is delivered to them.
ReceiptTypeSender ReceiptType = "sender"
// ReceiptTypeRetry means the message was delivered to the device, but decrypting the message failed.
ReceiptTypeRetry ReceiptType = "retry"
// ReceiptTypeRead means the user opened the chat and saw the message.
ReceiptTypeRead ReceiptType = "read"
// ReceiptTypeReadSelf means the current user read a message from a different device, and has read receipts disabled in privacy settings.
ReceiptTypeReadSelf ReceiptType = "read-self"
// ReceiptTypePlayed means the user opened a view-once media message.
//
// This is dispatched for both incoming and outgoing messages when played. If the current user opened the media,
// it means the media should be removed from all devices. If a recipient opened the media, it's just a notification
// for the sender that the media was viewed.
ReceiptTypePlayed ReceiptType = "played"
// ReceiptTypePlayedSelf probably means the current user opened a view-once media message from a different device,
// and has read receipts disabled in privacy settings.
ReceiptTypePlayedSelf ReceiptType = "played-self"
ReceiptTypeServerError ReceiptType = "server-error"
ReceiptTypeInactive ReceiptType = "inactive"
ReceiptTypePeerMsg ReceiptType = "peer_msg"
ReceiptTypeHistorySync ReceiptType = "hist_sync"
)
// GoString returns the name of the Go constant for the ReceiptType value.
func (rt ReceiptType) GoString() string {
switch rt {
case ReceiptTypeRead:
return "types.ReceiptTypeRead"
case ReceiptTypeReadSelf:
return "types.ReceiptTypeReadSelf"
case ReceiptTypeDelivered:
return "types.ReceiptTypeDelivered"
case ReceiptTypePlayed:
return "types.ReceiptTypePlayed"
default:
return fmt.Sprintf("types.ReceiptType(%#v)", string(rt))
}
}

View File

@ -28,11 +28,11 @@ type UserInfo struct {
// ProfilePictureInfo contains the ID and URL for a WhatsApp user's profile picture or group's photo.
type ProfilePictureInfo struct {
URL string // The full URL for the image, can be downloaded with a simple HTTP request.
ID string // The ID of the image. This is the same as UserInfo.PictureID.
Type string // The type of image. Known types include "image" (full res) and "preview" (thumbnail).
URL string `json:"url"` // The full URL for the image, can be downloaded with a simple HTTP request.
ID string `json:"id"` // The ID of the image. This is the same as UserInfo.PictureID.
Type string `json:"type"` // The type of image. Known types include "image" (full res) and "preview" (thumbnail).
DirectPath string // The path to the image, probably not very useful
DirectPath string `json:"direct_path"` // The path to the image, probably not very useful
}
// ContactInfo contains the cached names of a WhatsApp user.
@ -87,19 +87,37 @@ type PrivacySetting string
// Possible privacy setting values.
const (
PrivacySettingUndefined PrivacySetting = ""
PrivacySettingAll PrivacySetting = "all"
PrivacySettingContacts PrivacySetting = "contacts"
PrivacySettingNone PrivacySetting = "none"
PrivacySettingUndefined PrivacySetting = ""
PrivacySettingAll PrivacySetting = "all"
PrivacySettingContacts PrivacySetting = "contacts"
PrivacySettingContactBlacklist PrivacySetting = "contact_blacklist"
PrivacySettingMatchLastSeen PrivacySetting = "match_last_seen"
PrivacySettingKnown PrivacySetting = "known"
PrivacySettingNone PrivacySetting = "none"
)
// PrivacySettingType is the type of privacy setting.
type PrivacySettingType string
const (
PrivacySettingTypeGroupAdd PrivacySettingType = "groupadd" // Valid values: PrivacySettingAll, PrivacySettingContacts, PrivacySettingContactBlacklist, PrivacySettingNone
PrivacySettingTypeLastSeen PrivacySettingType = "last" // Valid values: PrivacySettingAll, PrivacySettingContacts, PrivacySettingContactBlacklist, PrivacySettingNone
PrivacySettingTypeStatus PrivacySettingType = "status" // Valid values: PrivacySettingAll, PrivacySettingContacts, PrivacySettingContactBlacklist, PrivacySettingNone
PrivacySettingTypeProfile PrivacySettingType = "profile" // Valid values: PrivacySettingAll, PrivacySettingContacts, PrivacySettingContactBlacklist, PrivacySettingNone
PrivacySettingTypeReadReceipts PrivacySettingType = "readreceipts" // Valid values: PrivacySettingAll, PrivacySettingNone
PrivacySettingTypeOnline PrivacySettingType = "online" // Valid values: PrivacySettingAll, PrivacySettingMatchLastSeen
PrivacySettingTypeCallAdd PrivacySettingType = "calladd" // Valid values: PrivacySettingAll, PrivacySettingKnown
)
// PrivacySettings contains the user's privacy settings.
type PrivacySettings struct {
GroupAdd PrivacySetting
LastSeen PrivacySetting
Status PrivacySetting
Profile PrivacySetting
ReadReceipts PrivacySetting
GroupAdd PrivacySetting // Valid values: PrivacySettingAll, PrivacySettingContacts, PrivacySettingContactBlacklist, PrivacySettingNone
LastSeen PrivacySetting // Valid values: PrivacySettingAll, PrivacySettingContacts, PrivacySettingContactBlacklist, PrivacySettingNone
Status PrivacySetting // Valid values: PrivacySettingAll, PrivacySettingContacts, PrivacySettingContactBlacklist, PrivacySettingNone
Profile PrivacySetting // Valid values: PrivacySettingAll, PrivacySettingContacts, PrivacySettingContactBlacklist, PrivacySettingNone
ReadReceipts PrivacySetting // Valid values: PrivacySettingAll, PrivacySettingNone
CallAdd PrivacySetting // Valid values: PrivacySettingAll, PrivacySettingKnown
Online PrivacySetting // Valid values: PrivacySettingAll, PrivacySettingMatchLastSeen
}
// StatusPrivacyType is the type of list in StatusPrivacy.
@ -121,3 +139,34 @@ type StatusPrivacy struct {
IsDefault bool
}
// Blocklist contains the user's current list of blocked users.
type Blocklist struct {
DHash string // TODO is this just a timestamp?
JIDs []JID
}
// BusinessHoursConfig contains business operating hours of a WhatsApp business.
type BusinessHoursConfig struct {
DayOfWeek string
Mode string
OpenTime string
CloseTime string
}
// Category contains a WhatsApp business category.
type Category struct {
ID string
Name string
}
// BusinessProfile contains the profile information of a WhatsApp business.
type BusinessProfile struct {
JID JID
Address string
Email string
Categories []Category
ProfileOptions map[string]string
BusinessHoursTimeZone string
BusinessHours []BusinessHoursConfig
}

View File

@ -17,15 +17,18 @@ import (
"net/http"
"net/url"
"go.mau.fi/util/random"
"go.mau.fi/whatsmeow/socket"
"go.mau.fi/whatsmeow/util/cbcutil"
"go.mau.fi/whatsmeow/util/randbytes"
)
// UploadResponse contains the data from the attachment upload, which can be put into a message to send the attachment.
type UploadResponse struct {
URL string `json:"url"`
DirectPath string `json:"direct_path"`
Handle string `json:"handle"`
ObjectID string `json:"object_id"`
MediaKey []byte `json:"-"`
FileEncSHA256 []byte `json:"-"`
@ -62,7 +65,7 @@ type UploadResponse struct {
// The same applies to the other message types like DocumentMessage, just replace the struct type and Message field name.
func (cli *Client) Upload(ctx context.Context, plaintext []byte, appInfo MediaType) (resp UploadResponse, err error) {
resp.FileLength = uint64(len(plaintext))
resp.MediaKey = randbytes.Make(32)
resp.MediaKey = random.Bytes(32)
plaintextSHA256 := sha256.Sum256(plaintext)
resp.FileSHA256 = plaintextSHA256[:]
@ -81,41 +84,99 @@ func (cli *Client) Upload(ctx context.Context, plaintext []byte, appInfo MediaTy
h.Write(ciphertext)
dataToUpload := append(ciphertext, h.Sum(nil)[:10]...)
fileEncSHA256 := sha256.Sum256(dataToUpload)
resp.FileEncSHA256 = fileEncSHA256[:]
dataHash := sha256.Sum256(dataToUpload)
resp.FileEncSHA256 = dataHash[:]
var mediaConn *MediaConn
mediaConn, err = cli.refreshMediaConn(false)
err = cli.rawUpload(ctx, dataToUpload, resp.FileEncSHA256, appInfo, false, &resp)
return
}
// UploadNewsletter uploads the given attachment to WhatsApp servers without encrypting it first.
//
// Newsletter media works mostly the same way as normal media, with a few differences:
// * Since it's unencrypted, there's no MediaKey or FileEncSha256 fields.
// * There's a "media handle" that needs to be passed in SendRequestExtra.
//
// Example:
//
// resp, err := cli.UploadNewsletter(context.Background(), yourImageBytes, whatsmeow.MediaImage)
// // handle error
//
// imageMsg := &waProto.ImageMessage{
// // Caption, mime type and other such fields work like normal
// Caption: proto.String("Hello, world!"),
// Mimetype: proto.String("image/png"),
//
// // URL and direct path are also there like normal media
// Url: &resp.URL,
// DirectPath: &resp.DirectPath,
// FileSha256: resp.FileSha256,
// FileLength: &resp.FileLength,
// // Newsletter media isn't encrypted, so the media key and file enc sha fields are not applicable
// }
// _, err = cli.SendMessage(context.Background(), newsletterJID, &waProto.Message{
// ImageMessage: imageMsg,
// }, whatsmeow.SendRequestExtra{
// // Unlike normal media, newsletters also include a "media handle" in the send request.
// MediaHandle: resp.Handle,
// })
// // handle error again
func (cli *Client) UploadNewsletter(ctx context.Context, data []byte, appInfo MediaType) (resp UploadResponse, err error) {
resp.FileLength = uint64(len(data))
hash := sha256.Sum256(data)
resp.FileSHA256 = hash[:]
err = cli.rawUpload(ctx, data, resp.FileSHA256, appInfo, true, &resp)
return
}
func (cli *Client) rawUpload(ctx context.Context, dataToUpload, fileHash []byte, appInfo MediaType, newsletter bool, resp *UploadResponse) error {
mediaConn, err := cli.refreshMediaConn(false)
if err != nil {
err = fmt.Errorf("failed to refresh media connections: %w", err)
return
return fmt.Errorf("failed to refresh media connections: %w", err)
}
token := base64.URLEncoding.EncodeToString(resp.FileEncSHA256)
token := base64.URLEncoding.EncodeToString(fileHash)
q := url.Values{
"auth": []string{mediaConn.Auth},
"token": []string{token},
}
mmsType := mediaTypeToMMSType[appInfo]
uploadPrefix := "mms"
if cli.MessengerConfig != nil {
uploadPrefix = "wa-msgr/mms"
// Messenger upload only allows voice messages, not audio files
if mmsType == "audio" {
mmsType = "ptt"
}
}
if newsletter {
mmsType = fmt.Sprintf("newsletter-%s", mmsType)
uploadPrefix = "newsletter"
}
var host string
// Hacky hack to prefer last option (rupload.facebook.com) for messenger uploads.
// For some reason, the primary host doesn't work, even though it has the <upload/> tag.
if cli.MessengerConfig != nil {
host = mediaConn.Hosts[len(mediaConn.Hosts)-1].Hostname
} else {
host = mediaConn.Hosts[0].Hostname
}
uploadURL := url.URL{
Scheme: "https",
Host: mediaConn.Hosts[0].Hostname,
Path: fmt.Sprintf("/mms/%s/%s", mmsType, token),
Host: host,
Path: fmt.Sprintf("/%s/%s/%s", uploadPrefix, mmsType, token),
RawQuery: q.Encode(),
}
var req *http.Request
req, err = http.NewRequestWithContext(ctx, http.MethodPost, uploadURL.String(), bytes.NewReader(dataToUpload))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL.String(), bytes.NewReader(dataToUpload))
if err != nil {
err = fmt.Errorf("failed to prepare request: %w", err)
return
return fmt.Errorf("failed to prepare request: %w", err)
}
req.Header.Set("Origin", socket.Origin)
req.Header.Set("Referer", socket.Origin+"/")
var httpResp *http.Response
httpResp, err = cli.http.Do(req)
httpResp, err := cli.http.Do(req)
if err != nil {
err = fmt.Errorf("failed to execute request: %w", err)
} else if httpResp.StatusCode != http.StatusOK {
@ -126,5 +187,5 @@ func (cli *Client) Upload(ctx context.Context, plaintext []byte, appInfo MediaTy
if httpResp != nil {
_ = httpResp.Body.Close()
}
return
return err
}

View File

@ -24,6 +24,7 @@ const BusinessMessageLinkPrefix = "https://wa.me/message/"
const ContactQRLinkPrefix = "https://wa.me/qr/"
const BusinessMessageLinkDirectPrefix = "https://api.whatsapp.com/message/"
const ContactQRLinkDirectPrefix = "https://api.whatsapp.com/qr/"
const NewsletterLinkPrefix = "https://whatsapp.com/channel/"
// ResolveBusinessMessageLink resolves a business message short link and returns the target JID, business name and
// text to prefill in the input field (if any).
@ -226,6 +227,91 @@ func (cli *Client) GetUserInfo(jids []types.JID) (map[types.JID]types.UserInfo,
return respData, nil
}
func (cli *Client) parseBusinessProfile(node *waBinary.Node) (*types.BusinessProfile, error) {
profileNode := node.GetChildByTag("profile")
jid, ok := profileNode.AttrGetter().GetJID("jid", true)
if !ok {
return nil, errors.New("missing jid in business profile")
}
address := string(profileNode.GetChildByTag("address").Content.([]byte))
email := string(profileNode.GetChildByTag("email").Content.([]byte))
businessHour := profileNode.GetChildByTag("business_hours")
businessHourTimezone := businessHour.AttrGetter().String("timezone")
businessHoursConfigs := businessHour.GetChildren()
businessHours := make([]types.BusinessHoursConfig, 0)
for _, config := range businessHoursConfigs {
if config.Tag != "business_hours_config" {
continue
}
dow := config.AttrGetter().String("dow")
mode := config.AttrGetter().String("mode")
openTime := config.AttrGetter().String("open_time")
closeTime := config.AttrGetter().String("close_time")
businessHours = append(businessHours, types.BusinessHoursConfig{
DayOfWeek: dow,
Mode: mode,
OpenTime: openTime,
CloseTime: closeTime,
})
}
categoriesNode := profileNode.GetChildByTag("categories")
categories := make([]types.Category, 0)
for _, category := range categoriesNode.GetChildren() {
if category.Tag != "category" {
continue
}
id := category.AttrGetter().String("id")
name := string(category.Content.([]byte))
categories = append(categories, types.Category{
ID: id,
Name: name,
})
}
profileOptionsNode := profileNode.GetChildByTag("profile_options")
profileOptions := make(map[string]string)
for _, option := range profileOptionsNode.GetChildren() {
profileOptions[option.Tag] = string(option.Content.([]byte))
}
return &types.BusinessProfile{
JID: jid,
Email: email,
Address: address,
Categories: categories,
ProfileOptions: profileOptions,
BusinessHoursTimeZone: businessHourTimezone,
BusinessHours: businessHours,
}, nil
}
// GetBusinessProfile gets the profile info of a WhatsApp business account
func (cli *Client) GetBusinessProfile(jid types.JID) (*types.BusinessProfile, error) {
resp, err := cli.sendIQ(infoQuery{
Type: iqGet,
To: types.ServerJID,
Namespace: "w:biz",
Content: []waBinary.Node{{
Tag: "business_profile",
Attrs: waBinary.Attrs{
"v": "244",
},
Content: []waBinary.Node{{
Tag: "profile",
Attrs: waBinary.Attrs{
"jid": jid,
},
}},
}},
})
if err != nil {
return nil, err
}
node, ok := resp.GetOptionalChildByTag("business_profile")
if !ok {
return nil, &ElementMissingError{Tag: "business_profile", In: "response to business profile query"}
}
return cli.parseBusinessProfile(&node)
}
// GetUserDevices gets the list of devices that the given user has. The input should be a list of
// regular JIDs, and the output will be a list of AD JIDs. The local device will not be included in
// the output even if the user's JID is included in the input. All other devices will be included.
@ -237,34 +323,50 @@ func (cli *Client) GetUserDevicesContext(ctx context.Context, jids []types.JID)
cli.userDevicesCacheLock.Lock()
defer cli.userDevicesCacheLock.Unlock()
var devices, jidsToSync []types.JID
var devices, jidsToSync, fbJIDsToSync []types.JID
for _, jid := range jids {
cached, ok := cli.userDevicesCache[jid]
if ok && len(cached) > 0 {
devices = append(devices, cached...)
if ok && len(cached.devices) > 0 {
devices = append(devices, cached.devices...)
} else if jid.Server == types.MessengerServer {
fbJIDsToSync = append(fbJIDsToSync, jid)
} else {
jidsToSync = append(jidsToSync, jid)
}
}
if len(jidsToSync) == 0 {
return devices, nil
}
list, err := cli.usync(ctx, jidsToSync, "query", "message", []waBinary.Node{
{Tag: "devices", Attrs: waBinary.Attrs{"version": "2"}},
})
if err != nil {
return nil, err
}
for _, user := range list.GetChildren() {
jid, jidOK := user.Attrs["jid"].(types.JID)
if user.Tag != "user" || !jidOK {
continue
if len(jidsToSync) > 0 {
list, err := cli.usync(ctx, jidsToSync, "query", "message", []waBinary.Node{
{Tag: "devices", Attrs: waBinary.Attrs{"version": "2"}},
})
if err != nil {
return nil, err
}
for _, user := range list.GetChildren() {
jid, jidOK := user.Attrs["jid"].(types.JID)
if user.Tag != "user" || !jidOK {
continue
}
userDevices := parseDeviceList(jid.User, user.GetChildByTag("devices"))
cli.userDevicesCache[jid] = deviceCache{devices: userDevices, dhash: participantListHashV2(userDevices)}
devices = append(devices, userDevices...)
}
}
if len(fbJIDsToSync) > 0 {
list, err := cli.getFBIDDevices(ctx, fbJIDsToSync)
if err != nil {
return nil, err
}
for _, user := range list.GetChildren() {
jid, jidOK := user.Attrs["jid"].(types.JID)
if user.Tag != "user" || !jidOK {
continue
}
userDevices := parseFBDeviceList(jid, user.GetChildByTag("devices"))
cli.userDevicesCache[jid] = userDevices
devices = append(devices, userDevices.devices...)
}
userDevices := parseDeviceList(jid.User, user.GetChildByTag("devices"))
cli.userDevicesCache[jid] = userDevices
devices = append(devices, userDevices...)
}
return devices, nil
@ -472,13 +574,56 @@ func parseDeviceList(user string, deviceNode waBinary.Node) []types.JID {
return devices
}
func parseFBDeviceList(user types.JID, deviceList waBinary.Node) deviceCache {
children := deviceList.GetChildren()
devices := make([]types.JID, 0, len(children))
for _, device := range children {
deviceID, ok := device.AttrGetter().GetInt64("id", true)
if device.Tag != "device" || !ok {
continue
}
user.Device = uint16(deviceID)
devices = append(devices, user)
// TODO take identities here too?
}
// TODO do something with the icdc blob?
return deviceCache{
devices: devices,
dhash: deviceList.AttrGetter().String("dhash"),
}
}
func (cli *Client) getFBIDDevices(ctx context.Context, jids []types.JID) (*waBinary.Node, error) {
users := make([]waBinary.Node, len(jids))
for i, jid := range jids {
users[i].Tag = "user"
users[i].Attrs = waBinary.Attrs{"jid": jid}
// TODO include dhash for users
}
resp, err := cli.sendIQ(infoQuery{
Context: ctx,
Namespace: "fbid:devices",
Type: iqGet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "users",
Content: users,
}},
})
if err != nil {
return nil, fmt.Errorf("failed to send usync query: %w", err)
} else if list, ok := resp.GetOptionalChildByTag("users"); !ok {
return nil, &ElementMissingError{Tag: "users", In: "response to fbid devices query"}
} else {
return &list, err
}
}
func (cli *Client) usync(ctx context.Context, jids []types.JID, mode, context string, query []waBinary.Node) (*waBinary.Node, error) {
userList := make([]waBinary.Node, len(jids))
for i, jid := range jids {
userList[i].Tag = "user"
if jid.AD {
jid.AD = false
}
jid = jid.ToNonAD()
switch jid.Server {
case types.LegacyUserServer:
userList[i].Content = []waBinary.Node{{
@ -519,3 +664,58 @@ func (cli *Client) usync(ctx context.Context, jids []types.JID, mode, context st
return &list, err
}
}
func (cli *Client) parseBlocklist(node *waBinary.Node) *types.Blocklist {
output := &types.Blocklist{
DHash: node.AttrGetter().String("dhash"),
}
for _, child := range node.GetChildren() {
ag := child.AttrGetter()
blockedJID := ag.JID("jid")
if !ag.OK() {
cli.Log.Debugf("Ignoring contact blocked data with unexpected attributes: %v", ag.Error())
continue
}
output.JIDs = append(output.JIDs, blockedJID)
}
return output
}
// GetBlocklist gets the list of users that this user has blocked.
func (cli *Client) GetBlocklist() (*types.Blocklist, error) {
resp, err := cli.sendIQ(infoQuery{
Namespace: "blocklist",
Type: iqGet,
To: types.ServerJID,
})
if err != nil {
return nil, err
}
list, ok := resp.GetOptionalChildByTag("list")
if !ok {
return nil, &ElementMissingError{Tag: "list", In: "response to blocklist query"}
}
return cli.parseBlocklist(&list), nil
}
// UpdateBlocklist updates the user's block list and returns the updated list.
func (cli *Client) UpdateBlocklist(jid types.JID, action events.BlocklistChangeAction) (*types.Blocklist, error) {
resp, err := cli.sendIQ(infoQuery{
Namespace: "blocklist",
Type: iqSet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "item",
Attrs: waBinary.Attrs{
"jid": jid,
"action": string(action),
},
}},
})
list, ok := resp.GetOptionalChildByTag("list")
if !ok {
return nil, &ElementMissingError{Tag: "list", In: "response to blocklist update"}
}
return cli.parseBlocklist(&list), err
}

View File

@ -0,0 +1,25 @@
package cbcutil
import (
"bytes"
"testing"
)
func TestEncryptDecrypt(t *testing.T) {
key := []byte("MySecretSecretSecretSecretKey123")
plain := []byte("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.")
cipher, err := Encrypt(key, nil, plain)
if err != nil {
t.Fail()
}
p, err := Decrypt(key, nil, cipher)
if err != nil {
t.Fail()
}
if !bytes.Equal(plain, p) {
t.Fail()
}
}

View File

@ -9,9 +9,8 @@ package keys
import (
"go.mau.fi/libsignal/ecc"
"go.mau.fi/util/random"
"golang.org/x/crypto/curve25519"
"go.mau.fi/whatsmeow/util/randbytes"
)
type KeyPair struct {
@ -31,7 +30,7 @@ func NewKeyPairFromPrivateKey(priv [32]byte) *KeyPair {
}
func NewKeyPair() *KeyPair {
priv := *(*[32]byte)(randbytes.Make(32))
priv := *(*[32]byte)(random.Bytes(32))
priv[0] &= 248
priv[31] &= 127

View File

@ -0,0 +1,38 @@
// Copyright (c) 2024 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package waLog
import (
"fmt"
"github.com/rs/zerolog"
)
type zeroLogger struct {
mod string
zerolog.Logger
}
// Zerolog wraps a [zerolog.Logger] to implement the [Logger] interface.
//
// Subloggers will be created by setting the `sublogger` field in the log context.
func Zerolog(log zerolog.Logger) Logger {
return &zeroLogger{Logger: log}
}
func (z *zeroLogger) Warnf(msg string, args ...any) { z.Warn().Msgf(msg, args...) }
func (z *zeroLogger) Errorf(msg string, args ...any) { z.Error().Msgf(msg, args...) }
func (z *zeroLogger) Infof(msg string, args ...any) { z.Info().Msgf(msg, args...) }
func (z *zeroLogger) Debugf(msg string, args ...any) { z.Debug().Msgf(msg, args...) }
func (z *zeroLogger) Sub(module string) Logger {
if z.mod != "" {
module = fmt.Sprintf("%s/%s", z.mod, module)
}
return &zeroLogger{mod: module, Logger: z.Logger.With().Str("sublogger", module).Logger()}
}
var _ Logger = &zeroLogger{}