mirror of
https://github.com/mautrix/whatsapp.git
synced 2026-07-06 14:23:20 -04:00
Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7753bb3999 | ||
|
|
f3ca7ac1c3 | ||
|
|
d5cf334423 |
||
|
|
0c7f5233c8 | ||
|
|
80b2c16b3d | ||
|
|
41ce80f583 | ||
|
|
d90c7d94d1 |
||
|
|
879fc7cb41 |
9 changed files with 139 additions and 19 deletions
4
go.mod
4
go.mod
|
|
@ -12,13 +12,13 @@ require (
|
|||
github.com/tidwall/gjson v1.19.0
|
||||
go.mau.fi/util v0.9.10
|
||||
go.mau.fi/webp v0.3.0
|
||||
go.mau.fi/whatsmeow v0.0.0-20260616120636-eaa388b4e537
|
||||
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b
|
||||
golang.org/x/image v0.42.0
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/sync v0.21.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
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 (
|
||||
|
|
|
|||
8
go.sum
8
go.sum
|
|
@ -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/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/whatsmeow v0.0.0-20260616120636-eaa388b4e537 h1:LhxtFShA1NXJWz4yNygwtl3Eycp9XsfMqJeDdcLyKmk=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260616120636-eaa388b4e537/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b h1:ZUk1ErarDNpnbosXR/MeOz2gkqA4S1bh8zjaSRj7N+Y=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U=
|
||||
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
|
||||
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
|
||||
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=
|
||||
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/mautrix v0.28.1 h1:Hic3oDMPbLbQu1fhboTRAKZcORMjzzkjxsa+SGk60b0=
|
||||
maunium.net/go/mautrix v0.28.1/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=
|
||||
maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895 h1:oe87/mP1vo4LnA6CJDEfyg5ZiAOar2quJNypfO8eS4Y=
|
||||
maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=
|
||||
|
|
|
|||
|
|
@ -400,8 +400,8 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
|||
return
|
||||
}
|
||||
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
|
||||
if errors.Is(err, whatsmeow.ErrNotInGroup) {
|
||||
log.Debug().Stringer("chat_jid", conv.ChatJID).
|
||||
if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) {
|
||||
log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID).
|
||||
Msg("Skipping creating room because the user is not a participant")
|
||||
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
|
||||
//if err != nil {
|
||||
|
|
|
|||
|
|
@ -126,6 +126,20 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
success = wa.handleWANewsletterLeave(evt)
|
||||
case *events.Picture:
|
||||
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:
|
||||
wa.handleWAAppStateSyncComplete(ctx, evt)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package connector
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
|
@ -12,6 +13,7 @@ import (
|
|||
"go.mau.fi/util/exsync"
|
||||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
waLog "go.mau.fi/whatsmeow/util/log"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
|
|
@ -25,6 +27,7 @@ const (
|
|||
LoginStepIDQR = "fi.mau.whatsapp.login.qr"
|
||||
LoginStepIDPhoneNumber = "fi.mau.whatsapp.login.phone"
|
||||
LoginStepIDCode = "fi.mau.whatsapp.login.code"
|
||||
LoginStepIDPasskey = "fi.mau.whatsapp.login.passkey"
|
||||
LoginStepIDComplete = "fi.mau.whatsapp.login.complete"
|
||||
|
||||
LoginFlowIDQR = "qr"
|
||||
|
|
@ -91,9 +94,11 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User,
|
|||
Bool("phone_code", flowID == LoginFlowIDPhone).
|
||||
Logger(),
|
||||
|
||||
WaitForQRs: exsync.NewEvent(),
|
||||
LoginComplete: exsync.NewEvent(),
|
||||
Received515: exsync.NewEvent(),
|
||||
WaitForQRs: exsync.NewEvent(),
|
||||
LoginComplete: exsync.NewEvent(),
|
||||
PasskeyRequest: exsync.NewEvent(),
|
||||
PasskeyConfirmation: exsync.NewEvent(),
|
||||
Received515: exsync.NewEvent(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +119,11 @@ type WALogin struct {
|
|||
Received515 *exsync.Event
|
||||
PrevQRIndex atomic.Int32
|
||||
|
||||
PasskeyRequest *exsync.Event
|
||||
PasskeyRequestData *events.PairPasskeyRequest
|
||||
PasskeyConfirmation *exsync.Event
|
||||
PasskeyConfirmationData *events.PairPasskeyConfirmation
|
||||
|
||||
Closed atomic.Bool
|
||||
EventHandlerID uint32
|
||||
}
|
||||
|
|
@ -122,6 +132,7 @@ var (
|
|||
_ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil)
|
||||
_ bridgev2.LoginProcessUserInput = (*WALogin)(nil)
|
||||
_ bridgev2.LoginProcessWithOverride = (*WALogin)(nil)
|
||||
_ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil)
|
||||
)
|
||||
|
||||
const LoginConnectWait = 15 * time.Second
|
||||
|
|
@ -262,6 +273,17 @@ func (wl *WALogin) handleEvent(rawEvt any) {
|
|||
case *events.ClientOutdated:
|
||||
wl.Log.Error().Msg("Got client outdated error")
|
||||
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:
|
||||
wl.Log.Info().Any("event_data", evt).Msg("Got pair successful event")
|
||||
wl.LoginSuccess = evt
|
||||
|
|
@ -289,10 +311,14 @@ func (wl *WALogin) handleEvent(rawEvt any) {
|
|||
|
||||
func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
if wl.PhoneCode {
|
||||
err := wl.LoginComplete.Wait(ctx)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
wl.Cancel()
|
||||
return nil, err
|
||||
return nil, ctx.Err()
|
||||
case <-wl.PasskeyRequest.GetChan():
|
||||
return wl.makePasskeyStep()
|
||||
case <-wl.LoginComplete.GetChan():
|
||||
// continue
|
||||
}
|
||||
} else {
|
||||
prevIndex := int(wl.PrevQRIndex.Load())
|
||||
|
|
@ -319,9 +345,16 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
|||
case <-ctx.Done():
|
||||
wl.Cancel()
|
||||
return nil, ctx.Err()
|
||||
case <-wl.PasskeyRequest.GetChan():
|
||||
return wl.makePasskeyStep()
|
||||
case <-wl.LoginComplete.GetChan():
|
||||
// continue
|
||||
}
|
||||
}
|
||||
return wl.onLoginComplete(ctx)
|
||||
}
|
||||
|
||||
func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
if wl.LoginError != nil {
|
||||
wl.Log.Debug().Err(wl.LoginError).Msg("Login completed with error")
|
||||
wl.Cancel()
|
||||
|
|
@ -372,6 +405,66 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
|||
}, 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() {
|
||||
wl.Closed.Store(true)
|
||||
wl.Client.RemoveEventHandler(wl.EventHandlerID)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,13 @@ func (wa *WhatsAppClient) FillBridgeState(state status.BridgeState) status.Bridg
|
|||
state.Error = WAPhoneOffline
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ var ResyncLoopInterval = 4 * time.Hour
|
|||
var ResyncJitterSeconds = 3600
|
||||
|
||||
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
|
||||
}
|
||||
wa.resyncQueueLock.Lock()
|
||||
|
|
@ -43,6 +44,7 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
|
|||
wa.UserLogin.Log.Debug().
|
||||
Stringer("jid", jid).
|
||||
Str("next_resync_in", nextResyncIn).
|
||||
Time("last_ghost_resync", lastSync).
|
||||
Msg("Enqueued resync for ghost")
|
||||
}
|
||||
wa.resyncQueueLock.Unlock()
|
||||
|
|
@ -50,7 +52,8 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
|
|||
|
||||
func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM bool) {
|
||||
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
|
||||
} else if !allowDM && jid.Server != types.GroupServer {
|
||||
return
|
||||
|
|
@ -61,6 +64,7 @@ func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM b
|
|||
wa.UserLogin.Log.Debug().
|
||||
Stringer("jid", jid).
|
||||
Stringer("next_resync_in", time.Until(wa.nextResync)).
|
||||
Time("last_portal_resync", lastSync).
|
||||
Msg("Enqueued resync for portal")
|
||||
}
|
||||
wa.resyncQueueLock.Unlock()
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exfmt"
|
||||
"go.mau.fi/util/exmime"
|
||||
"go.mau.fi/util/exslices"
|
||||
"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) {
|
||||
parts := make([]string, 0, 2)
|
||||
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 {
|
||||
parts = append(parts, fmt.Sprintf("%d videos", msg.GetExpectedVideoCount()))
|
||||
parts = append(parts, exfmt.Pluralizable("video")(int(msg.GetExpectedVideoCount())))
|
||||
}
|
||||
var partDesc string
|
||||
if len(parts) > 0 {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ type UserLoginMetadata struct {
|
|||
|
||||
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"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue