mirror of
https://github.com/mautrix/whatsapp.git
synced 2026-11-10 08:09:19 -05:00
196 lines
6.3 KiB
Go
196 lines
6.3 KiB
Go
package connector
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"runtime/debug"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rs/zerolog"
|
|
"go.mau.fi/util/exerrors"
|
|
"go.mau.fi/whatsmeow"
|
|
"go.mau.fi/whatsmeow/appstate"
|
|
"go.mau.fi/whatsmeow/proto/waAICommon"
|
|
"go.mau.fi/whatsmeow/proto/waE2E"
|
|
"go.mau.fi/whatsmeow/types"
|
|
"google.golang.org/protobuf/proto"
|
|
"maunium.net/go/mautrix/bridgev2"
|
|
"maunium.net/go/mautrix/bridgev2/database"
|
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
|
"maunium.net/go/mautrix/bridgev2/simplevent"
|
|
|
|
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
|
)
|
|
|
|
type museImage struct {
|
|
DirectPath string `json:"direct_path"`
|
|
EncHash []byte `json:"file_enc_sha256_b64"`
|
|
Hash []byte `json:"file_sha256_b64"`
|
|
Key []byte `json:"media_key_b64"`
|
|
Length int `json:"file_length"`
|
|
}
|
|
|
|
func (wa *WhatsAppClient) resyncWASARootSecrets(ctx context.Context) {
|
|
defer func() {
|
|
v := recover()
|
|
if v != nil {
|
|
zerolog.Ctx(ctx).Err(exerrors.RecoverToError(v)).
|
|
Bytes("stack", debug.Stack()).
|
|
Msg("Error resyncing WASA root secrets")
|
|
}
|
|
}()
|
|
wa.wasaResyncLock.Lock()
|
|
defer wa.wasaResyncLock.Unlock()
|
|
if wa.offlineSyncWaiter.Load() != nil || !wa.Client.IsConnected() {
|
|
return
|
|
}
|
|
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
|
|
if meta.WASAResynced {
|
|
return
|
|
}
|
|
log := zerolog.Ctx(ctx)
|
|
log.Info().Msg("Resyncing WASA root secrets for existing login")
|
|
success := true
|
|
if err := wa.Client.FetchAppState(ctx, appstate.WAPatchRegularHigh, true, false); err != nil {
|
|
log.Err(err).Msg("Failed to resync WASA root secrets")
|
|
// Mark the sync as completed on mismatching LTHash errors, because those can get permanently stuck
|
|
// (retrying won't help). In case it's not stuck, a single call should be enough to request a resync.
|
|
if !errors.Is(err, appstate.ErrMismatchingLTHash) || ctx.Err() != nil {
|
|
return
|
|
}
|
|
success = false
|
|
}
|
|
meta.WASAResynced = true
|
|
if err := wa.UserLogin.Save(ctx); err != nil {
|
|
meta.WASAResynced = false
|
|
log.Err(err).Msg("Failed to save WASA resync completion")
|
|
return
|
|
}
|
|
if success {
|
|
log.Info().Msg("Completed WASA root secret resync")
|
|
} else {
|
|
log.Info().Msg("Marked WASA root resync as done despite errors")
|
|
}
|
|
}
|
|
|
|
func (wa *WhatsAppClient) requestMuseProfile(ctx context.Context) {
|
|
defer func() {
|
|
v := recover()
|
|
if v != nil {
|
|
zerolog.Ctx(ctx).Err(exerrors.RecoverToError(v)).
|
|
Bytes("stack", debug.Stack()).
|
|
Msg("Error requesting Muse profile")
|
|
}
|
|
}()
|
|
if !wa.museProfileLock.TryLock() {
|
|
return
|
|
}
|
|
defer wa.museProfileLock.Unlock()
|
|
if wa.offlineSyncWaiter.Load() != nil || !wa.Client.IsConnected() || time.Since(wa.lastMuseProfileRequest) < 15*time.Minute {
|
|
return
|
|
}
|
|
portal, err := wa.Main.Bridge.DB.Portal.GetByKey(ctx, wa.makeWAPortalKey(types.MuseJID))
|
|
if err != nil {
|
|
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to check Muse profile")
|
|
return
|
|
} else if portal != nil && portal.NameIsCustom && portal.Name != "" && portal.NameSet && museAvatarReady(portal) {
|
|
return
|
|
}
|
|
rootID, err := wa.GetStore().ChatSettings.GetWASARootSecretID(ctx, types.MuseJID)
|
|
if err != nil {
|
|
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to check Muse root secret")
|
|
return
|
|
} else if rootID == "" {
|
|
return
|
|
}
|
|
zerolog.Ctx(ctx).Info().Msg("Sending request to sync Muse profile")
|
|
wa.lastMuseProfileRequest = time.Now()
|
|
_, err = wa.Client.SendMessage(ctx, types.MuseJID, &waE2E.Message{
|
|
ProtocolMessage: &waE2E.ProtocolMessage{
|
|
Type: waE2E.ProtocolMessage_AI_METADATA_OPERATION.Enum(),
|
|
AiMetadataOperation: &waAICommon.AIMetadataOperation{
|
|
HatchMetadataSync: &waAICommon.HatchMetadataSync{
|
|
Data: []byte(`{"version":1,"type":"req","payload":{"method":"channel.bootstrap","params":{"sections":["agent.status","identity.updated","hitl.snapshot"]}}}`),
|
|
TimestampMS: proto.Int64(time.Now().UnixMilli()),
|
|
RequestID: proto.String(uuid.NewString()),
|
|
},
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to request Muse profile")
|
|
}
|
|
}
|
|
|
|
func museAvatarReady(portal *database.Portal) bool {
|
|
expectedHash, err := base64.StdEncoding.DecodeString(string(portal.AvatarID))
|
|
return err != nil || len(expectedHash) != 32 ||
|
|
(portal.AvatarMXC != "" && portal.AvatarHash == [32]byte(expectedHash))
|
|
}
|
|
|
|
func (wa *WhatsAppClient) handleMuseMetadata(ctx context.Context, data []byte) bool {
|
|
var msg struct {
|
|
Type string `json:"type"`
|
|
Payload struct {
|
|
Event string `json:"event"`
|
|
Payload struct {
|
|
Name string `json:"name"`
|
|
Avatar struct {
|
|
Image museImage `json:"secure_image"`
|
|
} `json:"avatar"`
|
|
} `json:"payload"`
|
|
} `json:"payload"`
|
|
}
|
|
if err := json.Unmarshal(data, &msg); err != nil {
|
|
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to decode Muse metadata")
|
|
return true
|
|
}
|
|
if msg.Type != "event" || msg.Payload.Event != "identity.updated" || msg.Payload.Payload.Name == "" {
|
|
return true
|
|
}
|
|
name := msg.Payload.Payload.Name
|
|
avatar := wa.museAvatar(msg.Payload.Payload.Avatar.Image)
|
|
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
|
|
EventMeta: simplevent.EventMeta{
|
|
Type: bridgev2.RemoteEventChatInfoChange,
|
|
PortalKey: wa.makeWAPortalKey(types.MuseJID),
|
|
CreatePortal: true,
|
|
PreHandleFunc: func(_ context.Context, portal *bridgev2.Portal) {
|
|
if avatar != nil && !museAvatarReady(portal.Portal) {
|
|
portal.AvatarSet = false
|
|
}
|
|
},
|
|
},
|
|
ChatInfoChange: &bridgev2.ChatInfoChange{
|
|
ChatInfo: &bridgev2.ChatInfo{
|
|
Name: &name,
|
|
Avatar: avatar,
|
|
ExcludeChangesFromTimeline: true,
|
|
},
|
|
},
|
|
}).Success
|
|
}
|
|
|
|
func (wa *WhatsAppClient) museAvatar(image museImage) *bridgev2.Avatar {
|
|
if image.DirectPath == "" || image.Length <= 0 || image.Length > 5<<20 {
|
|
return nil
|
|
}
|
|
if len(image.EncHash) != 32 || len(image.Hash) != 32 || len(image.Key) != 32 {
|
|
return nil
|
|
}
|
|
return &bridgev2.Avatar{
|
|
ID: networkid.AvatarID(base64.StdEncoding.EncodeToString(image.Hash)),
|
|
Get: func(ctx context.Context) ([]byte, error) {
|
|
data, err := wa.Client.DownloadMediaWithPath(ctx, image.DirectPath, image.EncHash, image.Hash, image.Key, whatsmeow.MediaImage, "image", false)
|
|
if err != nil {
|
|
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to download Muse avatar")
|
|
return nil, fmt.Errorf("failed to download Muse avatar: %w", err)
|
|
}
|
|
return data, nil
|
|
},
|
|
}
|
|
}
|