1
0
Fork 0
mirror of https://github.com/mautrix/whatsapp.git synced 2026-07-11 08:26:08 -04:00

Compare commits

..

12 commits

Author SHA1 Message Date
Tulir Asokan
5250ddfa7d msgconv/from-whatsapp: handle v5 and v6 poll creations 2026-07-10 18:36:17 +03:00
Tulir Asokan
a1a4cf87b3 msgconv/from-matrix: add hack to use LID for message secrets in DMs 2026-07-09 14:44:00 +03:00
Tulir Asokan
942d1b4c0a dependencies: update whatsmeow 2026-07-09 14:27:54 +03:00
Tulir Asokan
8d34f6f6bd msgconv/from-whatsapp: use correct message info for decrypting poll votes 2026-07-09 12:12:18 +03:00
Tulir Asokan
7753bb3999 dependencies: update mautrix-go 2026-07-02 15:40:33 +03:00
Tulir Asokan
f3ca7ac1c3 dependencies: update mautrix-go 2026-07-02 00:15:15 +03:00
Tulir Asokan
d5cf334423
login: add support for passkeys (#936) 2026-07-01 23:01:34 +03:00
Tulir Asokan
0c7f5233c8 handlewhatsapp: send analytics for timelock event 2026-06-22 15:55:40 +03:00
Tulir Asokan
80b2c16b3d userinfo: log last sync time when enqueuing resync 2026-06-22 15:55:40 +03:00
Tulir Asokan
41ce80f583 dependencies: update whatsmeow 2026-06-22 15:55:40 +03:00
Kishan Bagaria
d90c7d94d1
msgconv/wa-media: pluralize album media counts (#933) 2026-06-18 11:53:06 -07:00
Nick Mills-Barrett
879fc7cb41
backfill: skip groups that 404 on info (#929) 2026-06-17 14:15:56 +01:00
16 changed files with 185 additions and 40 deletions

4
go.mod
View file

@ -12,13 +12,13 @@ require (
github.com/tidwall/gjson v1.19.0 github.com/tidwall/gjson v1.19.0
go.mau.fi/util v0.9.10 go.mau.fi/util v0.9.10
go.mau.fi/webp v0.3.0 go.mau.fi/webp v0.3.0
go.mau.fi/whatsmeow v0.0.0-20260616120636-eaa388b4e537 go.mau.fi/whatsmeow v0.0.0-20260709092057-73fe7355f59f
golang.org/x/image v0.42.0 golang.org/x/image v0.42.0
golang.org/x/net v0.56.0 golang.org/x/net v0.56.0
golang.org/x/sync v0.21.0 golang.org/x/sync v0.21.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.28.1 maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895
) )
require ( require (

8
go.sum
View file

@ -75,8 +75,8 @@ go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg=
go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A= go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A=
go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA=
go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo=
go.mau.fi/whatsmeow v0.0.0-20260616120636-eaa388b4e537 h1:LhxtFShA1NXJWz4yNygwtl3Eycp9XsfMqJeDdcLyKmk= go.mau.fi/whatsmeow v0.0.0-20260709092057-73fe7355f59f h1:VZkwFBEQ9TbB9IWsldw8BJDhHbEBElm9VD2VQXbSc9s=
go.mau.fi/whatsmeow v0.0.0-20260616120636-eaa388b4e537/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U= go.mau.fi/whatsmeow v0.0.0-20260709092057-73fe7355f59f/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.28.1 h1:Hic3oDMPbLbQu1fhboTRAKZcORMjzzkjxsa+SGk60b0= maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895 h1:oe87/mP1vo4LnA6CJDEfyg5ZiAOar2quJNypfO8eS4Y=
maunium.net/go/mautrix v0.28.1/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY= maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=

View file

@ -400,8 +400,8 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
return return
} }
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true) wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
if errors.Is(err, whatsmeow.ErrNotInGroup) { if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) {
log.Debug().Stringer("chat_jid", conv.ChatJID). log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID).
Msg("Skipping creating room because the user is not a participant") Msg("Skipping creating room because the user is not a participant")
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID) //err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
//if err != nil { //if err != nil {
@ -750,7 +750,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
// TODO use proper intent // TODO use proper intent
intent := wa.Main.Bridge.Bot intent := wa.Main.Bridge.Bot
wrapped := &bridgev2.BackfillMessage{ wrapped := &bridgev2.BackfillMessage{
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil), ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, nil, isViewOnce, true, nil),
Sender: wa.makeEventSender(ctx, info.Sender), Sender: wa.makeEventSender(ctx, info.Sender),
ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID), ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID),
TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)), TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)),

View file

@ -52,6 +52,7 @@ func (wa *WhatsAppClient) getPortalKeyByMessageSource(ms types.MessageSource) ne
} }
type MessageInfoWrapper struct { type MessageInfoWrapper struct {
OrigSource types.MessageSource
Info types.MessageInfo Info types.MessageInfo
wa *WhatsAppClient wa *WhatsAppClient
} }
@ -200,7 +201,7 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
ctx = context.WithValue(ctx, msgconv.ContextKeyEditTargetID, evt.Message.GetProtocolMessage().GetKey().GetID()) ctx = context.WithValue(ctx, msgconv.ContextKeyEditTargetID, evt.Message.GetProtocolMessage().GetKey().GetID())
cm := evt.wa.Main.MsgConv.ToMatrix( cm := evt.wa.Main.MsgConv.ToMatrix(
ctx, portal, evt.wa.Client, intent, editedMsg, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, previouslyConvertedPart, ctx, portal, evt.wa.Client, intent, editedMsg, evt.MsgEvent.RawMessage, &evt.Info, &evt.OrigSource, evt.isViewOnce(), false, previouslyConvertedPart,
) )
if evt.isUndecryptableUpsertSubEvent && isFailedMedia(cm) { if evt.isUndecryptableUpsertSubEvent && isFailedMedia(cm) {
evt.postHandle = func() { evt.postHandle = func() {
@ -285,7 +286,7 @@ func (evt *WAMessageEvent) HandleExisting(ctx context.Context, portal *bridgev2.
func (evt *WAMessageEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) { func (evt *WAMessageEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) {
evt.wa.EnqueuePortalResync(portal, false) evt.wa.EnqueuePortalResync(portal, false)
converted := evt.wa.Main.MsgConv.ToMatrix( converted := evt.wa.Main.MsgConv.ToMatrix(
ctx, portal, evt.wa.Client, intent, evt.Message, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, nil, ctx, portal, evt.wa.Client, intent, evt.Message, evt.MsgEvent.RawMessage, &evt.Info, &evt.OrigSource, evt.isViewOnce(), false, nil,
) )
if isFailedMedia(converted) { if isFailedMedia(converted) {
evt.postHandle = func() { evt.postHandle = func() {

View file

@ -177,7 +177,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
var req whatsmeow.SendRequestExtra var req whatsmeow.SendRequestExtra
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup { if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(wa.Client, messageID), reactionMsg.ReactionMessage) reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(ctx, wa.Client, messageID), reactionMsg.ReactionMessage)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to encrypt reaction: %w", err) return nil, fmt.Errorf("failed to encrypt reaction: %w", err)
} }

View file

@ -126,6 +126,20 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
success = wa.handleWANewsletterLeave(evt) success = wa.handleWANewsletterLeave(evt)
case *events.Picture: case *events.Picture:
success = wa.handleWAPictureUpdate(ctx, evt) success = wa.handleWAPictureUpdate(ctx, evt)
case *events.NotifyAccountReachoutTimelock:
wa.UserLogin.TrackAnalytics("WhatsApp Account Reachout Timelock", map[string]any{
"enforcement_type": evt.EnforcementType,
"is_active": evt.IsActive,
"time_enforcement_ends": evt.TimeEnforcementEnds.Time,
})
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil = evt.TimeEnforcementEnds.Time
if wa.UserLogin.BridgeState.GetPrevUnsent().StateEvent == status.StateConnected {
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
}
err := wa.UserLogin.Save(ctx)
if err != nil {
log.Err(err).Msg("Failed to save user login metadata after reachout timelock update")
}
case *events.AppStateSyncComplete: case *events.AppStateSyncComplete:
wa.handleWAAppStateSyncComplete(ctx, evt) wa.handleWAAppStateSyncComplete(ctx, evt)
@ -344,6 +358,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
evt.UnwrapRaw() evt.UnwrapRaw()
parsedMessageType = getMessageType(evt.Message) parsedMessageType = getMessageType(evt.Message)
} }
origSource := evt.Info.MessageSource
wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID) wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID)
wa.UserLogin.Log.Trace(). wa.UserLogin.Log.Trace().
Any("info", evt.Info). Any("info", evt.Info).
@ -395,6 +410,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{ res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{
MessageInfoWrapper: &MessageInfoWrapper{ MessageInfoWrapper: &MessageInfoWrapper{
OrigSource: origSource,
Info: evt.Info, Info: evt.Info,
wa: wa, wa: wa,
}, },

View file

@ -2,6 +2,7 @@ package connector
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -12,6 +13,7 @@ import (
"go.mau.fi/util/exsync" "go.mau.fi/util/exsync"
"go.mau.fi/util/jsontime" "go.mau.fi/util/jsontime"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events" "go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log" waLog "go.mau.fi/whatsmeow/util/log"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
@ -25,6 +27,7 @@ const (
LoginStepIDQR = "fi.mau.whatsapp.login.qr" LoginStepIDQR = "fi.mau.whatsapp.login.qr"
LoginStepIDPhoneNumber = "fi.mau.whatsapp.login.phone" LoginStepIDPhoneNumber = "fi.mau.whatsapp.login.phone"
LoginStepIDCode = "fi.mau.whatsapp.login.code" LoginStepIDCode = "fi.mau.whatsapp.login.code"
LoginStepIDPasskey = "fi.mau.whatsapp.login.passkey"
LoginStepIDComplete = "fi.mau.whatsapp.login.complete" LoginStepIDComplete = "fi.mau.whatsapp.login.complete"
LoginFlowIDQR = "qr" LoginFlowIDQR = "qr"
@ -93,6 +96,8 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User,
WaitForQRs: exsync.NewEvent(), WaitForQRs: exsync.NewEvent(),
LoginComplete: exsync.NewEvent(), LoginComplete: exsync.NewEvent(),
PasskeyRequest: exsync.NewEvent(),
PasskeyConfirmation: exsync.NewEvent(),
Received515: exsync.NewEvent(), Received515: exsync.NewEvent(),
}, nil }, nil
} }
@ -114,6 +119,11 @@ type WALogin struct {
Received515 *exsync.Event Received515 *exsync.Event
PrevQRIndex atomic.Int32 PrevQRIndex atomic.Int32
PasskeyRequest *exsync.Event
PasskeyRequestData *events.PairPasskeyRequest
PasskeyConfirmation *exsync.Event
PasskeyConfirmationData *events.PairPasskeyConfirmation
Closed atomic.Bool Closed atomic.Bool
EventHandlerID uint32 EventHandlerID uint32
} }
@ -122,6 +132,7 @@ var (
_ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil) _ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil)
_ bridgev2.LoginProcessUserInput = (*WALogin)(nil) _ bridgev2.LoginProcessUserInput = (*WALogin)(nil)
_ bridgev2.LoginProcessWithOverride = (*WALogin)(nil) _ bridgev2.LoginProcessWithOverride = (*WALogin)(nil)
_ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil)
) )
const LoginConnectWait = 15 * time.Second const LoginConnectWait = 15 * time.Second
@ -262,6 +273,17 @@ func (wl *WALogin) handleEvent(rawEvt any) {
case *events.ClientOutdated: case *events.ClientOutdated:
wl.Log.Error().Msg("Got client outdated error") wl.Log.Error().Msg("Got client outdated error")
wl.LoginError = ErrLoginClientOutdated wl.LoginError = ErrLoginClientOutdated
case *events.PairPasskeyRequest:
wl.PasskeyRequestData = evt
wl.PasskeyRequest.Set()
return
case *events.PairPasskeyConfirmation:
wl.PasskeyConfirmationData = evt
wl.PasskeyConfirmation.Set()
return
case *events.PairPasskeyError:
wl.Log.Err(evt.Error).Msg("Got passkey error")
wl.LoginError = evt.Error
case *events.PairSuccess: case *events.PairSuccess:
wl.Log.Info().Any("event_data", evt).Msg("Got pair successful event") wl.Log.Info().Any("event_data", evt).Msg("Got pair successful event")
wl.LoginSuccess = evt wl.LoginSuccess = evt
@ -289,10 +311,14 @@ func (wl *WALogin) handleEvent(rawEvt any) {
func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
if wl.PhoneCode { if wl.PhoneCode {
err := wl.LoginComplete.Wait(ctx) select {
if err != nil { case <-ctx.Done():
wl.Cancel() wl.Cancel()
return nil, err return nil, ctx.Err()
case <-wl.PasskeyRequest.GetChan():
return wl.makePasskeyStep()
case <-wl.LoginComplete.GetChan():
// continue
} }
} else { } else {
prevIndex := int(wl.PrevQRIndex.Load()) prevIndex := int(wl.PrevQRIndex.Load())
@ -319,9 +345,16 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
case <-ctx.Done(): case <-ctx.Done():
wl.Cancel() wl.Cancel()
return nil, ctx.Err() return nil, ctx.Err()
case <-wl.PasskeyRequest.GetChan():
return wl.makePasskeyStep()
case <-wl.LoginComplete.GetChan(): case <-wl.LoginComplete.GetChan():
// continue
} }
} }
return wl.onLoginComplete(ctx)
}
func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, error) {
if wl.LoginError != nil { if wl.LoginError != nil {
wl.Log.Debug().Err(wl.LoginError).Msg("Login completed with error") wl.Log.Debug().Err(wl.LoginError).Msg("Login completed with error")
wl.Cancel() wl.Cancel()
@ -372,6 +405,66 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
}, nil }, nil
} }
func (wl *WALogin) makePasskeyStep() (*bridgev2.LoginStep, error) {
pubkeyData, err := json.Marshal(wl.PasskeyRequestData.PublicKey)
if err != nil {
wl.Log.Err(err).Msg("Failed to marshal public key data")
return nil, err
}
return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeWebAuthn,
StepID: LoginStepIDPasskey,
WebAuthnParams: &bridgev2.LoginWebAuthnParams{
URL: "https://web.whatsapp.com",
PublicKey: pubkeyData,
},
}, nil
}
func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawMessage) (*bridgev2.LoginStep, error) {
var resp types.WebAuthnResponse
err := json.Unmarshal(rawResp, &resp)
if err != nil {
wl.Log.Err(err).Msg("Failed to unmarshal WebAuthn response")
wl.Cancel()
return nil, err
}
err = wl.Client.SendPasskeyResponse(ctx, &resp)
if err != nil {
wl.Log.Err(err).Msg("Failed to send WebAuthn response")
wl.Cancel()
return nil, err
}
select {
case <-ctx.Done():
wl.Cancel()
return nil, ctx.Err()
case <-wl.PasskeyConfirmation.GetChan():
if !wl.PasskeyConfirmationData.SkipHandoffUX {
wl.Cancel()
return nil, bridgev2.RespError{
ErrCode: "FI.MAU.WHATSAPP.NOT_IMPLEMENTED",
Err: "Displaying WebAuthn pairing confirmation codes is not yet implemented",
StatusCode: http.StatusBadRequest,
}
}
err = wl.Client.SendPasskeyConfirmation(ctx)
if err != nil {
wl.Log.Err(err).Msg("Failed to send WebAuthn confirmation")
wl.Cancel()
return nil, err
}
case <-wl.LoginComplete.GetChan():
}
select {
case <-ctx.Done():
wl.Cancel()
return nil, ctx.Err()
case <-wl.LoginComplete.GetChan():
}
return wl.onLoginComplete(ctx)
}
func (wl *WALogin) Cancel() { func (wl *WALogin) Cancel() {
wl.Closed.Store(true) wl.Closed.Store(true)
wl.Client.RemoveEventHandler(wl.EventHandlerID) wl.Client.RemoveEventHandler(wl.EventHandlerID)

View file

@ -37,6 +37,13 @@ func (wa *WhatsAppClient) FillBridgeState(state status.BridgeState) status.Bridg
state.Error = WAPhoneOffline state.Error = WAPhoneOffline
state.UserAction = status.UserActionOpenNative state.UserAction = status.UserActionOpenNative
} }
rtu := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil
if !rtu.IsZero() {
if state.Info == nil {
state.Info = make(map[string]any)
}
state.Info["reachout_timelock_until"] = rtu
}
return state return state
} }

View file

@ -29,7 +29,8 @@ var ResyncLoopInterval = 4 * time.Hour
var ResyncJitterSeconds = 3600 var ResyncJitterSeconds = 3600
func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) { func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
if ghost.Metadata.(*waid.GhostMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) { lastSync := ghost.Metadata.(*waid.GhostMetadata).LastSync.Time
if lastSync.Add(ResyncMinInterval).After(time.Now()) {
return return
} }
wa.resyncQueueLock.Lock() wa.resyncQueueLock.Lock()
@ -43,6 +44,7 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
wa.UserLogin.Log.Debug(). wa.UserLogin.Log.Debug().
Stringer("jid", jid). Stringer("jid", jid).
Str("next_resync_in", nextResyncIn). Str("next_resync_in", nextResyncIn).
Time("last_ghost_resync", lastSync).
Msg("Enqueued resync for ghost") Msg("Enqueued resync for ghost")
} }
wa.resyncQueueLock.Unlock() wa.resyncQueueLock.Unlock()
@ -50,7 +52,8 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM bool) { func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM bool) {
jid, _ := waid.ParsePortalID(portal.ID) jid, _ := waid.ParsePortalID(portal.ID)
if portal.Metadata.(*waid.PortalMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) { lastSync := portal.Metadata.(*waid.PortalMetadata).LastSync.Time
if lastSync.Add(ResyncMinInterval).After(time.Now()) {
return return
} else if !allowDM && jid.Server != types.GroupServer { } else if !allowDM && jid.Server != types.GroupServer {
return return
@ -61,6 +64,7 @@ func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM b
wa.UserLogin.Log.Debug(). wa.UserLogin.Log.Debug().
Stringer("jid", jid). Stringer("jid", jid).
Stringer("next_resync_in", time.Until(wa.nextResync)). Stringer("next_resync_in", time.Until(wa.nextResync)).
Time("last_portal_resync", lastSync).
Msg("Enqueued resync for portal") Msg("Enqueued resync for portal")
} }
wa.resyncQueueLock.Unlock() wa.resyncQueueLock.Unlock()

View file

@ -67,10 +67,11 @@ func getMessageType(waMsg *waE2E.Message) string {
return "encrypted comment" return "encrypted comment"
case waMsg.CommentMessage != nil: case waMsg.CommentMessage != nil:
return "comment" return "comment"
case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil: case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil ||
waMsg.PollCreationMessageV5 != nil || waMsg.PollCreationMessageV6 != nil:
return "poll create" return "poll create"
case waMsg.PollCreationMessageV4 != nil || waMsg.PollCreationMessageV5 != nil: case waMsg.PollCreationMessageV4 != nil:
return "poll create (vNext)" return "poll create (v4)"
case waMsg.PollUpdateMessage != nil: case waMsg.PollUpdateMessage != nil:
return "poll update" return "poll update"
case waMsg.ProtocolMessage != nil: case waMsg.ProtocolMessage != nil:

View file

@ -143,7 +143,7 @@ func (mc *MessageConverter) ToWhatsApp(
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to parse message ID: %w", err) return nil, nil, fmt.Errorf("failed to parse message ID: %w", err)
} }
rootMsgInfo := MessageIDToInfo(client, parsedID) rootMsgInfo := MessageIDToInfo(ctx, client, parsedID)
message, err = client.EncryptComment(ctx, rootMsgInfo, message) message, err = client.EncryptComment(ctx, rootMsgInfo, message)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to encrypt comment: %w", err) return nil, nil, fmt.Errorf("failed to encrypt comment: %w", err)

View file

@ -136,6 +136,7 @@ func (mc *MessageConverter) ToMatrix(
waMsg *waE2E.Message, waMsg *waE2E.Message,
rawWaMsg *waE2E.Message, rawWaMsg *waE2E.Message,
info *types.MessageInfo, info *types.MessageInfo,
origSource *types.MessageSource,
isViewOnce bool, isViewOnce bool,
isBackfill bool, isBackfill bool,
previouslyConvertedPart *bridgev2.ConvertedMessagePart, previouslyConvertedPart *bridgev2.ConvertedMessagePart,
@ -175,8 +176,14 @@ func (mc *MessageConverter) ToMatrix(
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV2) part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV2)
case waMsg.PollCreationMessageV3 != nil: case waMsg.PollCreationMessageV3 != nil:
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV3) part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV3)
//case waMsg.PollCreationMessageV4 != nil:
// part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV4)
case waMsg.PollCreationMessageV5 != nil:
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV5)
case waMsg.PollCreationMessageV6 != nil:
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV6)
case waMsg.PollUpdateMessage != nil: case waMsg.PollUpdateMessage != nil:
part, contextInfo = mc.convertPollUpdateMessage(ctx, info, waMsg.PollUpdateMessage) part, contextInfo = mc.convertPollUpdateMessage(ctx, info, origSource, waMsg.PollUpdateMessage)
case waMsg.EventMessage != nil: case waMsg.EventMessage != nil:
part, contextInfo = mc.convertEventMessage(ctx, waMsg.EventMessage) part, contextInfo = mc.convertEventMessage(ctx, waMsg.EventMessage)
case waMsg.PinInChatMessage != nil: case waMsg.PinInChatMessage != nil:

View file

@ -115,7 +115,7 @@ func (mc *MessageConverter) PollVoteToWhatsApp(
zerolog.Ctx(ctx).Err(err).Msg("Failed to parse message ID") zerolog.Ctx(ctx).Err(err).Msg("Failed to parse message ID")
return nil, fmt.Errorf("failed to parse message ID") return nil, fmt.Errorf("failed to parse message ID")
} }
pollMsgInfo := MessageIDToInfo(client, parsedMsgID) pollMsgInfo := MessageIDToInfo(ctx, client, parsedMsgID)
pollMsgInfo.Type = "poll" pollMsgInfo.Type = "poll"
optionHashes := make([][]byte, 0, len(content.Response.Answers)) optionHashes := make([][]byte, 0, len(content.Response.Answers))
if pollMsg.Metadata.(*waid.MessageMetadata).IsMatrixPoll { if pollMsg.Metadata.(*waid.MessageMetadata).IsMatrixPoll {
@ -146,13 +146,23 @@ func (mc *MessageConverter) PollVoteToWhatsApp(
return &waE2E.Message{PollUpdateMessage: pollUpdate}, err return &waE2E.Message{PollUpdateMessage: pollUpdate}, err
} }
func MessageIDToInfo(client *whatsmeow.Client, parsedMsgID *waid.ParsedMessageID) *types.MessageInfo { func MessageIDToInfo(ctx context.Context, client *whatsmeow.Client, parsedMsgID *waid.ParsedMessageID) *types.MessageInfo {
chat := parsedMsgID.Chat
sender := parsedMsgID.Sender
if client.Store.LIDMigrationTimestamp != 0 && chat.Server == types.DefaultUserServer {
chatLID, _ := client.Store.LIDs.GetLIDForPN(ctx, chat)
senderLID, _ := client.Store.LIDs.GetLIDForPN(ctx, sender)
if !chatLID.IsEmpty() && !senderLID.IsEmpty() {
chat = chatLID
sender = senderLID
}
}
return &types.MessageInfo{ return &types.MessageInfo{
MessageSource: types.MessageSource{ MessageSource: types.MessageSource{
Chat: parsedMsgID.Chat, Chat: chat,
Sender: parsedMsgID.Sender, Sender: sender,
IsFromMe: parsedMsgID.Sender.User == client.Store.GetLID().User || parsedMsgID.Sender.User == client.Store.GetJID().User, IsFromMe: sender.User == client.Store.GetLID().User || sender.User == client.Store.GetJID().User,
IsGroup: parsedMsgID.Chat.Server == types.GroupServer, IsGroup: chat.Server == types.GroupServer,
}, },
ID: parsedMsgID.ID, ID: parsedMsgID.ID,
} }

View file

@ -27,6 +27,7 @@ import (
"strings" "strings"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exfmt"
"go.mau.fi/util/exmime" "go.mau.fi/util/exmime"
"go.mau.fi/util/exslices" "go.mau.fi/util/exslices"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
@ -129,10 +130,10 @@ func getMediaIDVersion(msg MediaMessage) []byte {
func (mc *MessageConverter) convertAlbumMessage(ctx context.Context, msg *waE2E.AlbumMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { func (mc *MessageConverter) convertAlbumMessage(ctx context.Context, msg *waE2E.AlbumMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
parts := make([]string, 0, 2) parts := make([]string, 0, 2)
if msg.GetExpectedImageCount() > 0 { if msg.GetExpectedImageCount() > 0 {
parts = append(parts, fmt.Sprintf("%d images", msg.GetExpectedImageCount())) parts = append(parts, exfmt.Pluralizable("image")(int(msg.GetExpectedImageCount())))
} }
if msg.GetExpectedVideoCount() > 0 { if msg.GetExpectedVideoCount() > 0 {
parts = append(parts, fmt.Sprintf("%d videos", msg.GetExpectedVideoCount())) parts = append(parts, exfmt.Pluralizable("video")(int(msg.GetExpectedVideoCount())))
} }
var partDesc string var partDesc string
if len(parts) > 0 { if len(parts) > 0 {

View file

@ -170,7 +170,7 @@ var failedPollUpdatePart = &bridgev2.ConvertedMessagePart{
DontBridge: true, DontBridge: true,
} }
func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *types.MessageInfo, msg *waE2E.PollUpdateMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *types.MessageInfo, origSource *types.MessageSource, msg *waE2E.PollUpdateMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
log := zerolog.Ctx(ctx) log := zerolog.Ctx(ctx)
pollMessageID := KeyToMessageID(ctx, getClient(ctx), info.Chat, info.Sender, msg.PollCreationMessageKey) pollMessageID := KeyToMessageID(ctx, getClient(ctx), info.Chat, info.Sender, msg.PollCreationMessageKey)
pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "") pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "")
@ -181,8 +181,12 @@ func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *
log.Warn().Str("target_message_id", string(pollMessageID)).Msg("Poll update target message not found") log.Warn().Str("target_message_id", string(pollMessageID)).Msg("Poll update target message not found")
return failedPollUpdatePart, nil return failedPollUpdatePart, nil
} }
infoForDecrypt := *info
if origSource != nil {
infoForDecrypt.MessageSource = *origSource
}
vote, err := getClient(ctx).DecryptPollVote(ctx, &events.Message{ vote, err := getClient(ctx).DecryptPollVote(ctx, &events.Message{
Info: *info, Info: infoForDecrypt,
Message: &waE2E.Message{PollUpdateMessage: msg}, Message: &waE2E.Message{PollUpdateMessage: msg},
}) })
if err != nil { if err != nil {

View file

@ -42,6 +42,7 @@ type UserLoginMetadata struct {
AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"` AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"`
HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"` HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"`
ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitempty"`
MData json.RawMessage `json:"mdata,omitempty"` MData json.RawMessage `json:"mdata,omitempty"`
} }