2024-08-13 14:11:10 +03:00
package connector
import (
2025-08-28 02:08:25 +02:00
"bytes"
2024-08-13 14:11:10 +03:00
"context"
2025-08-28 02:08:25 +02:00
"crypto/sha256"
2024-10-16 16:44:57 +03:00
"errors"
2024-08-13 14:11:10 +03:00
"fmt"
2025-08-28 02:08:25 +02:00
"image"
"image/jpeg"
2025-05-01 16:59:57 +03:00
"strings"
2024-09-04 05:46:27 +03:00
"time"
2024-08-13 14:11:10 +03:00
"github.com/rs/zerolog"
2025-04-08 21:00:38 +03:00
"go.mau.fi/util/ptr"
2024-08-13 14:11:10 +03:00
"go.mau.fi/util/variationselector"
"go.mau.fi/whatsmeow"
2025-10-02 15:14:19 +03:00
"go.mau.fi/whatsmeow/appstate"
"go.mau.fi/whatsmeow/proto/waCommon"
2024-08-13 14:11:10 +03:00
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
2025-08-28 02:08:25 +02:00
"golang.org/x/image/draw"
2024-08-13 14:11:10 +03:00
"google.golang.org/protobuf/proto"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
2024-10-16 16:44:57 +03:00
"maunium.net/go/mautrix/event"
2024-08-13 14:11:10 +03:00
2025-04-08 21:00:38 +03:00
"go.mau.fi/mautrix-whatsapp/pkg/msgconv"
2024-10-15 17:59:57 +03:00
"go.mau.fi/mautrix-whatsapp/pkg/waid"
2024-08-13 14:11:10 +03:00
)
var (
2025-08-25 18:15:50 +03:00
_ bridgev2 . TypingHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
_ bridgev2 . EditHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
_ bridgev2 . ReactionHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
_ bridgev2 . RedactionHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
_ bridgev2 . ReadReceiptHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
_ bridgev2 . PollHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
_ bridgev2 . DisappearTimerChangingNetworkAPI = ( * WhatsAppClient ) ( nil )
_ bridgev2 . MembershipHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
2025-08-28 02:08:25 +02:00
_ bridgev2 . RoomNameHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
_ bridgev2 . RoomTopicHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
_ bridgev2 . RoomAvatarHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
2025-10-02 15:14:19 +03:00
_ bridgev2 . MuteHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
_ bridgev2 . TagHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
_ bridgev2 . MarkedUnreadHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
2025-10-17 07:27:05 +08:00
_ bridgev2 . DeleteChatHandlingNetworkAPI = ( * WhatsAppClient ) ( nil )
2024-08-13 14:11:10 +03:00
)
2024-10-07 16:14:58 +03:00
func ( wa * WhatsAppClient ) HandleMatrixPollStart ( ctx context . Context , msg * bridgev2 . MatrixPollStart ) ( * bridgev2 . MatrixMessageResponse , error ) {
waMsg , optionMap , err := wa . Main . MsgConv . PollStartToWhatsApp ( ctx , msg . Content , msg . ReplyTo , msg . Portal )
if err != nil {
return nil , fmt . Errorf ( "failed to convert poll vote: %w" , err )
}
2025-04-08 21:00:38 +03:00
resp , err := wa . handleConvertedMatrixMessage ( ctx , & msg . MatrixMessage , waMsg , nil )
2024-10-07 16:14:58 +03:00
if err != nil {
return nil , err
}
resp . DB . Metadata . ( * waid . MessageMetadata ) . IsMatrixPoll = true
resp . PostSave = func ( ctx context . Context , message * database . Message ) {
err := wa . Main . DB . PollOption . Put ( ctx , message . MXID , optionMap )
if err != nil {
zerolog . Ctx ( ctx ) . Err ( err ) . Msg ( "Failed to save poll options" )
}
}
return resp , nil
}
func ( wa * WhatsAppClient ) HandleMatrixPollVote ( ctx context . Context , msg * bridgev2 . MatrixPollVote ) ( * bridgev2 . MatrixMessageResponse , error ) {
waMsg , err := wa . Main . MsgConv . PollVoteToWhatsApp ( ctx , wa . Client , msg . Content , msg . VoteTo )
if err != nil {
return nil , fmt . Errorf ( "failed to convert poll vote: %w" , err )
}
2025-04-08 21:00:38 +03:00
return wa . handleConvertedMatrixMessage ( ctx , & msg . MatrixMessage , waMsg , nil )
2024-10-07 16:14:58 +03:00
}
2024-08-13 14:11:10 +03:00
func ( wa * WhatsAppClient ) HandleMatrixMessage ( ctx context . Context , msg * bridgev2 . MatrixMessage ) ( * bridgev2 . MatrixMessageResponse , error ) {
2025-04-08 21:00:38 +03:00
waMsg , req , err := wa . Main . MsgConv . ToWhatsApp ( ctx , wa . Client , msg . Event , msg . Content , msg . ReplyTo , msg . ThreadRoot , msg . Portal )
2024-08-13 14:11:10 +03:00
if err != nil {
return nil , fmt . Errorf ( "failed to convert message: %w" , err )
}
2025-04-08 21:00:38 +03:00
return wa . handleConvertedMatrixMessage ( ctx , msg , waMsg , req )
2024-10-07 16:14:58 +03:00
}
2024-10-16 16:44:57 +03:00
var ErrBroadcastSendDisabled = bridgev2 . WrapErrorInStatus ( errors . New ( "sending status messages is disabled" ) ) . WithErrorAsMessage ( ) . WithIsCertain ( true ) . WithSendNotice ( true ) . WithErrorReason ( event . MessageStatusUnsupported )
var ErrBroadcastReactionUnsupported = bridgev2 . WrapErrorInStatus ( errors . New ( "reacting to status messages is not currently supported" ) ) . WithErrorAsMessage ( ) . WithIsCertain ( true ) . WithSendNotice ( true ) . WithErrorReason ( event . MessageStatusUnsupported )
2025-04-08 21:00:38 +03:00
func ( wa * WhatsAppClient ) handleConvertedMatrixMessage ( ctx context . Context , msg * bridgev2 . MatrixMessage , waMsg * waE2E . Message , req * whatsmeow . SendRequestExtra ) ( * bridgev2 . MatrixMessageResponse , error ) {
if req == nil {
req = & whatsmeow . SendRequestExtra { }
}
2025-05-01 16:59:57 +03:00
if strings . HasPrefix ( string ( msg . InputTransactionID ) , whatsmeow . WebMessageIDPrefix ) {
req . ID = types . MessageID ( msg . InputTransactionID )
} else {
req . ID = wa . Client . GenerateMessageID ( )
}
2024-09-06 17:41:26 +03:00
chatJID , err := waid . ParsePortalID ( msg . Portal . ID )
2024-08-13 14:11:10 +03:00
if err != nil {
return nil , err
}
2024-10-16 16:44:57 +03:00
if chatJID == types . StatusBroadcastJID && wa . Main . Config . DisableStatusBroadcastSend {
return nil , ErrBroadcastSendDisabled
}
2025-04-08 21:00:38 +03:00
wrappedMsgID := waid . MakeMessageID ( chatJID , wa . JID , req . ID )
2025-04-16 15:07:22 +03:00
wrappedMsgID2 := waid . MakeMessageID ( chatJID , wa . GetStore ( ) . GetLID ( ) , req . ID )
2024-09-06 17:41:26 +03:00
msg . AddPendingToIgnore ( networkid . TransactionID ( wrappedMsgID ) )
2025-04-08 21:00:38 +03:00
msg . AddPendingToIgnore ( networkid . TransactionID ( wrappedMsgID2 ) )
resp , err := wa . Client . SendMessage ( ctx , chatJID , waMsg , * req )
2024-08-13 14:11:10 +03:00
if err != nil {
return nil , err
}
2025-04-08 21:00:38 +03:00
var pickedMessageID networkid . MessageID
2025-11-11 18:22:25 +02:00
if resp . Sender == wa . GetStore ( ) . GetLID ( ) && chatJID . Server != types . DefaultUserServer {
2025-04-08 21:00:38 +03:00
pickedMessageID = wrappedMsgID2
msg . RemovePending ( networkid . TransactionID ( wrappedMsgID ) )
} else {
pickedMessageID = wrappedMsgID
msg . RemovePending ( networkid . TransactionID ( wrappedMsgID2 ) )
}
2024-08-13 14:11:10 +03:00
return & bridgev2 . MatrixMessageResponse {
DB : & database . Message {
2025-04-08 21:00:38 +03:00
ID : pickedMessageID ,
SenderID : waid . MakeUserID ( resp . Sender ) ,
2024-08-13 14:11:10 +03:00
Timestamp : resp . Timestamp ,
2024-09-16 15:38:39 +03:00
Metadata : & waid . MessageMetadata {
2024-09-10 15:09:08 +03:00
SenderDeviceID : wa . JID . Device ,
} ,
2024-08-13 14:11:10 +03:00
} ,
2024-10-01 17:32:58 +03:00
StreamOrder : resp . Timestamp . Unix ( ) ,
2025-04-08 21:00:38 +03:00
RemovePending : networkid . TransactionID ( pickedMessageID ) ,
2024-08-13 14:11:10 +03:00
} , nil
}
func ( wa * WhatsAppClient ) PreHandleMatrixReaction ( _ context . Context , msg * bridgev2 . MatrixReaction ) ( bridgev2 . MatrixReactionPreResponse , error ) {
2024-10-16 16:44:57 +03:00
portalJID , err := waid . ParsePortalID ( msg . Portal . ID )
if err != nil {
2025-04-21 15:03:33 +03:00
return bridgev2 . MatrixReactionPreResponse { } , fmt . Errorf ( "failed to parse portal ID: %w" , err )
2024-10-16 16:44:57 +03:00
} else if portalJID == types . StatusBroadcastJID {
return bridgev2 . MatrixReactionPreResponse { } , ErrBroadcastReactionUnsupported
}
2025-04-08 21:00:38 +03:00
sender := wa . JID
2025-04-09 00:25:37 +03:00
if portalJID . Server == types . HiddenUserServer ||
msg . Portal . Metadata . ( * waid . PortalMetadata ) . CommunityAnnouncementGroup ||
msg . Portal . Metadata . ( * waid . PortalMetadata ) . AddressingMode == types . AddressingModeLID {
2025-04-16 15:07:22 +03:00
sender = wa . GetStore ( ) . GetLID ( )
2025-04-08 21:00:38 +03:00
}
2024-08-13 14:11:10 +03:00
return bridgev2 . MatrixReactionPreResponse {
2025-04-08 21:00:38 +03:00
SenderID : waid . MakeUserID ( sender ) ,
2024-08-13 14:11:10 +03:00
Emoji : variationselector . Remove ( msg . Content . RelatesTo . Key ) ,
MaxReactions : 1 ,
} , nil
}
func ( wa * WhatsAppClient ) HandleMatrixReaction ( ctx context . Context , msg * bridgev2 . MatrixReaction ) ( * database . Reaction , error ) {
messageID , err := waid . ParseMessageID ( msg . TargetMessage . ID )
if err != nil {
2025-04-21 15:03:33 +03:00
return nil , fmt . Errorf ( "failed to parse target message ID: %w" , err )
2024-08-13 14:11:10 +03:00
}
2024-09-06 17:41:26 +03:00
portalJID , err := waid . ParsePortalID ( msg . Portal . ID )
2024-08-13 14:11:10 +03:00
if err != nil {
2025-04-21 15:03:33 +03:00
return nil , fmt . Errorf ( "failed to parse portal ID: %w" , err )
2024-08-13 14:11:10 +03:00
}
reactionMsg := & waE2E . Message {
ReactionMessage : & waE2E . ReactionMessage {
Key : wa . messageIDToKey ( messageID ) ,
Text : proto . String ( msg . PreHandleResp . Emoji ) ,
SenderTimestampMS : proto . Int64 ( msg . Event . Timestamp ) ,
} ,
}
2025-04-08 21:00:38 +03:00
var req whatsmeow . SendRequestExtra
if msg . Portal . Metadata . ( * waid . PortalMetadata ) . CommunityAnnouncementGroup {
2025-05-14 14:40:03 +03:00
reactionMsg . EncReactionMessage , err = wa . Client . EncryptReaction ( ctx , msgconv . MessageIDToInfo ( wa . Client , messageID ) , reactionMsg . ReactionMessage )
2025-04-08 21:00:38 +03:00
if err != nil {
return nil , fmt . Errorf ( "failed to encrypt reaction: %w" , err )
}
reactionMsg . ReactionMessage = nil
req . Meta = & types . MsgMetaInfo {
DeprecatedLIDSession : ptr . Ptr ( false ) ,
}
}
2024-08-13 14:11:10 +03:00
2025-04-08 21:00:38 +03:00
resp , err := wa . Client . SendMessage ( ctx , portalJID , reactionMsg , req )
2024-08-13 14:11:10 +03:00
zerolog . Ctx ( ctx ) . Trace ( ) . Any ( "response" , resp ) . Msg ( "WhatsApp reaction response" )
2024-09-10 15:09:08 +03:00
return & database . Reaction {
2024-09-16 15:38:39 +03:00
Metadata : & waid . ReactionMetadata {
2024-09-10 15:09:08 +03:00
SenderDeviceID : wa . JID . Device ,
} ,
} , err
2024-08-13 14:11:10 +03:00
}
func ( wa * WhatsAppClient ) HandleMatrixReactionRemove ( ctx context . Context , msg * bridgev2 . MatrixReactionRemove ) error {
messageID , err := waid . ParseMessageID ( msg . TargetReaction . MessageID )
if err != nil {
2025-04-21 15:03:33 +03:00
return fmt . Errorf ( "failed to parse target message ID: %w" , err )
2024-08-13 14:11:10 +03:00
}
2024-09-06 17:41:26 +03:00
portalJID , err := waid . ParsePortalID ( msg . Portal . ID )
2024-08-13 14:11:10 +03:00
if err != nil {
2025-04-21 15:03:33 +03:00
return fmt . Errorf ( "failed to parse portal ID: %w" , err )
2024-08-13 14:11:10 +03:00
}
reactionMsg := & waE2E . Message {
ReactionMessage : & waE2E . ReactionMessage {
Key : wa . messageIDToKey ( messageID ) ,
Text : proto . String ( "" ) ,
SenderTimestampMS : proto . Int64 ( msg . Event . Timestamp ) ,
} ,
}
2025-05-01 16:59:57 +03:00
extra := whatsmeow . SendRequestExtra { }
if strings . HasPrefix ( string ( msg . InputTransactionID ) , whatsmeow . WebMessageIDPrefix ) {
extra . ID = types . MessageID ( msg . InputTransactionID )
}
resp , err := wa . Client . SendMessage ( ctx , portalJID , reactionMsg , extra )
2024-08-13 14:11:10 +03:00
zerolog . Ctx ( ctx ) . Trace ( ) . Any ( "response" , resp ) . Msg ( "WhatsApp reaction response" )
return err
}
func ( wa * WhatsAppClient ) HandleMatrixEdit ( ctx context . Context , edit * bridgev2 . MatrixEdit ) error {
log := zerolog . Ctx ( ctx )
2025-05-01 16:59:57 +03:00
var editID types . MessageID
if strings . HasPrefix ( string ( edit . InputTransactionID ) , whatsmeow . WebMessageIDPrefix ) {
editID = types . MessageID ( edit . InputTransactionID )
} else {
editID = wa . Client . GenerateMessageID ( )
}
2024-09-06 17:41:26 +03:00
2024-08-13 14:11:10 +03:00
messageID , err := waid . ParseMessageID ( edit . EditTarget . ID )
if err != nil {
2025-04-21 15:03:33 +03:00
return fmt . Errorf ( "failed to parse target message ID: %w" , err )
2024-08-13 14:11:10 +03:00
}
2024-09-06 17:41:26 +03:00
portalJID , err := waid . ParsePortalID ( edit . Portal . ID )
2024-08-13 14:11:10 +03:00
if err != nil {
2025-04-21 15:03:33 +03:00
return fmt . Errorf ( "failed to parse portal ID: %w" , err )
2024-08-13 14:11:10 +03:00
}
2025-04-08 21:00:38 +03:00
waMsg , _ , err := wa . Main . MsgConv . ToWhatsApp ( ctx , wa . Client , edit . Event , edit . Content , nil , nil , edit . Portal )
2024-09-06 17:41:26 +03:00
if err != nil {
return fmt . Errorf ( "failed to convert message: %w" , err )
}
convertedEdit := wa . Client . BuildEdit ( messageID . Chat , messageID . ID , waMsg )
if edit . OrigSender == nil {
convertedEdit . EditedMessage . Message . ProtocolMessage . TimestampMS = proto . Int64 ( edit . Event . Timestamp )
}
2024-08-13 14:11:10 +03:00
2024-09-06 17:41:26 +03:00
//wrappedMsgID := waid.MakeMessageID(portalJID, wa.JID, messageID)
//edit.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
resp , err := wa . Client . SendMessage ( ctx , portalJID , convertedEdit , whatsmeow . SendRequestExtra {
ID : editID ,
} )
2024-08-13 14:11:10 +03:00
log . Trace ( ) . Any ( "response" , resp ) . Msg ( "WhatsApp edit response" )
return err
}
func ( wa * WhatsAppClient ) HandleMatrixMessageRemove ( ctx context . Context , msg * bridgev2 . MatrixMessageRemove ) error {
log := zerolog . Ctx ( ctx )
messageID , err := waid . ParseMessageID ( msg . TargetMessage . ID )
if err != nil {
2025-04-21 15:03:33 +03:00
return fmt . Errorf ( "failed to parse target message ID: %w" , err )
2024-08-13 14:11:10 +03:00
}
2024-09-06 17:41:26 +03:00
portalJID , err := waid . ParsePortalID ( msg . Portal . ID )
2024-08-13 14:11:10 +03:00
if err != nil {
2025-04-21 15:03:33 +03:00
return fmt . Errorf ( "failed to parse portal ID: %w" , err )
2024-08-13 14:11:10 +03:00
}
revokeMessage := wa . Client . BuildRevoke ( messageID . Chat , messageID . Sender , messageID . ID )
2025-05-01 16:59:57 +03:00
extra := whatsmeow . SendRequestExtra { }
if strings . HasPrefix ( string ( msg . InputTransactionID ) , whatsmeow . WebMessageIDPrefix ) {
extra . ID = types . MessageID ( msg . InputTransactionID )
}
resp , err := wa . Client . SendMessage ( ctx , portalJID , revokeMessage , extra )
2024-08-13 14:11:10 +03:00
log . Trace ( ) . Any ( "response" , resp ) . Msg ( "WhatsApp delete response" )
return err
}
2024-09-06 17:41:26 +03:00
func ( wa * WhatsAppClient ) HandleMatrixReadReceipt ( ctx context . Context , receipt * bridgev2 . MatrixReadReceipt ) error {
if ! receipt . ReadUpTo . After ( receipt . LastRead ) {
2024-09-04 05:46:27 +03:00
return nil
}
2024-09-06 17:41:26 +03:00
if receipt . LastRead . IsZero ( ) {
receipt . LastRead = receipt . ReadUpTo . Add ( - 5 * time . Second )
}
portalJID , err := waid . ParsePortalID ( receipt . Portal . ID )
2024-09-04 05:46:27 +03:00
if err != nil {
2025-04-21 15:03:33 +03:00
return fmt . Errorf ( "failed to parse portal ID: %w" , err )
2024-09-04 05:46:27 +03:00
}
2024-09-06 17:41:26 +03:00
messages , err := receipt . Portal . Bridge . DB . Message . GetMessagesBetweenTimeQuery ( ctx , receipt . Portal . PortalKey , receipt . LastRead , receipt . ReadUpTo )
if err != nil {
return fmt . Errorf ( "failed to get messages to mark as read: %w" , err )
} else if len ( messages ) == 0 {
return nil
}
log := zerolog . Ctx ( ctx )
log . Trace ( ) .
Time ( "last_read" , receipt . LastRead ) .
Time ( "read_up_to" , receipt . ReadUpTo ) .
Int ( "message_count" , len ( messages ) ) .
Msg ( "Handling read receipt" )
messagesToRead := make ( map [ types . JID ] [ ] string )
for _ , msg := range messages {
parsed , err := waid . ParseMessageID ( msg . ID )
if err != nil {
continue
}
2025-04-16 15:07:22 +03:00
if parsed . Sender . User == wa . GetStore ( ) . GetLID ( ) . User || parsed . Sender . User == wa . JID . User {
2024-09-06 17:41:26 +03:00
continue
}
var key types . JID
// In group chats, group receipts by sender. In DMs, just use blank key (no participant field).
if parsed . Sender != parsed . Chat {
key = parsed . Sender
}
messagesToRead [ key ] = append ( messagesToRead [ key ] , parsed . ID )
}
for messageSender , ids := range messagesToRead {
2025-10-27 16:20:47 +02:00
err = wa . Client . MarkRead ( ctx , ids , receipt . Receipt . Timestamp , portalJID , messageSender )
2024-09-06 17:41:26 +03:00
if err != nil {
log . Err ( err ) . Strs ( "ids" , ids ) . Msg ( "Failed to mark messages as read" )
}
}
2024-09-04 05:46:27 +03:00
return err
}
2024-09-06 17:41:26 +03:00
func ( wa * WhatsAppClient ) HandleMatrixTyping ( ctx context . Context , msg * bridgev2 . MatrixTyping ) error {
portalJID , err := waid . ParsePortalID ( msg . Portal . ID )
2024-09-04 05:46:27 +03:00
if err != nil {
return err
}
var chatPresence types . ChatPresence
var mediaPresence types . ChatPresenceMedia
if msg . IsTyping {
2024-09-06 17:41:26 +03:00
chatPresence = types . ChatPresenceComposing
2024-09-04 05:46:27 +03:00
} else {
2024-09-06 17:41:26 +03:00
chatPresence = types . ChatPresencePaused
2024-09-04 05:46:27 +03:00
}
switch msg . Type {
case bridgev2 . TypingTypeText :
2024-09-06 17:41:26 +03:00
mediaPresence = types . ChatPresenceMediaText
2024-09-04 05:46:27 +03:00
case bridgev2 . TypingTypeRecordingMedia :
2024-09-06 17:41:26 +03:00
mediaPresence = types . ChatPresenceMediaAudio
2024-09-04 05:46:27 +03:00
case bridgev2 . TypingTypeUploadingMedia :
return nil
}
if wa . Main . Config . SendPresenceOnTyping {
2025-10-27 16:20:47 +02:00
err = wa . updatePresence ( ctx , types . PresenceAvailable )
2024-09-04 05:46:27 +03:00
if err != nil {
2024-09-06 17:41:26 +03:00
zerolog . Ctx ( ctx ) . Warn ( ) . Err ( err ) . Msg ( "Failed to set presence on typing" )
2024-09-04 05:46:27 +03:00
}
}
2025-10-27 16:20:47 +02:00
return wa . Client . SendChatPresence ( ctx , portalJID , chatPresence , mediaPresence )
2024-08-13 14:11:10 +03:00
}
2025-03-19 20:18:28 +09:00
2025-08-25 18:15:50 +03:00
var errUnsupportedDisappearingTimer = bridgev2 . WrapErrorInStatus ( errors . New ( "invalid value for disappearing timer" ) ) . WithErrorAsMessage ( ) . WithIsCertain ( true ) . WithSendNotice ( true )
func ( wa * WhatsAppClient ) HandleMatrixDisappearingTimer ( ctx context . Context , msg * bridgev2 . MatrixDisappearingTimer ) ( bool , error ) {
portalJID , err := waid . ParsePortalID ( msg . Portal . ID )
if err != nil {
return false , err
}
switch msg . Content . Timer . Duration {
case whatsmeow . DisappearingTimerOff , whatsmeow . DisappearingTimer24Hours , whatsmeow . DisappearingTimer7Days , whatsmeow . DisappearingTimer90Days :
default :
return false , fmt . Errorf ( "%w (%s)" , errUnsupportedDisappearingTimer , msg . Content . Timer . Duration )
}
settingTS := time . UnixMilli ( msg . Event . Timestamp )
2025-10-27 16:20:47 +02:00
err = wa . Client . SetDisappearingTimer ( ctx , portalJID , msg . Content . Timer . Duration , settingTS )
2025-08-25 18:15:50 +03:00
if err != nil {
return false , err
}
msg . Portal . Metadata . ( * waid . PortalMetadata ) . DisappearingTimerSetAt = settingTS . Unix ( )
msg . Portal . Disappear = database . DisappearingSetting {
Type : event . DisappearingTypeAfterSend ,
Timer : msg . Content . Timer . Duration ,
}
if msg . Portal . Disappear . Timer == 0 {
msg . Portal . Disappear . Type = event . DisappearingTypeNone
}
return true , nil
}
2025-12-02 15:27:34 +02:00
func ( wa * WhatsAppClient ) HandleMatrixMembership ( ctx context . Context , msg * bridgev2 . MatrixMembershipChange ) ( * bridgev2 . MatrixMembershipResult , error ) {
2025-03-19 20:18:28 +09:00
portalJID , err := waid . ParsePortalID ( msg . Portal . ID )
if err != nil {
2025-12-02 15:27:34 +02:00
return nil , err
2025-03-19 20:18:28 +09:00
}
if msg . Portal . RoomType == database . RoomTypeDM {
switch msg . Type {
case bridgev2 . Invite :
2025-12-02 15:27:34 +02:00
return nil , fmt . Errorf ( "cannot invite additional user to dm" )
2025-03-19 20:18:28 +09:00
default :
2025-12-02 15:27:34 +02:00
return nil , nil
2025-03-19 20:18:28 +09:00
}
}
changes := make ( [ ] types . JID , 1 )
var action whatsmeow . ParticipantChange
switch msg . Type {
case bridgev2 . Invite :
action = whatsmeow . ParticipantChangeAdd
case bridgev2 . Leave , bridgev2 . Kick :
action = whatsmeow . ParticipantChangeRemove
default :
2025-12-02 15:27:34 +02:00
return nil , nil
2025-03-19 20:18:28 +09:00
}
switch target := msg . Target . ( type ) {
case * bridgev2 . Ghost :
changes [ 0 ] = waid . ParseUserID ( target . ID )
case * bridgev2 . UserLogin :
ghost , err := target . Bridge . GetGhostByID ( ctx , networkid . UserID ( target . ID ) )
if err != nil {
2025-12-02 15:27:34 +02:00
return nil , fmt . Errorf ( "failed to get ghost for user: %w" , err )
2025-03-19 20:18:28 +09:00
}
changes [ 0 ] = waid . ParseUserID ( ghost . ID )
default :
2025-12-02 15:27:34 +02:00
return nil , fmt . Errorf ( "cannot get target intent: unknown type: %T" , target )
2025-03-19 20:18:28 +09:00
}
2025-12-02 15:27:34 +02:00
resp , err := wa . Client . UpdateGroupParticipants ( ctx , portalJID , changes , action )
2025-03-19 20:18:28 +09:00
if err != nil {
2025-12-02 15:27:34 +02:00
return nil , err
} else if len ( resp ) == 0 {
return nil , fmt . Errorf ( "no response for participant change" )
} else if resp [ 0 ] . Error != 0 {
return nil , fmt . Errorf ( "failed to change participant: code %d" , resp [ 0 ] . Error )
2025-03-19 20:18:28 +09:00
}
2025-12-02 15:27:34 +02:00
zerolog . Ctx ( ctx ) . Debug ( ) .
Any ( "change_response" , resp ) .
Msg ( "Handled membership change" )
2025-03-19 20:18:28 +09:00
2025-12-02 15:27:34 +02:00
return & bridgev2 . MatrixMembershipResult { RedirectTo : waid . MakeUserID ( resp [ 0 ] . JID ) } , nil
2025-03-19 20:18:28 +09:00
}
2025-08-28 02:08:25 +02:00
func ( wa * WhatsAppClient ) HandleMatrixRoomName ( ctx context . Context , msg * bridgev2 . MatrixRoomName ) ( bool , error ) {
portalJID , err := waid . ParsePortalID ( msg . Portal . ID )
if err != nil {
return false , err
}
if msg . Portal . RoomType == database . RoomTypeDM {
return false , fmt . Errorf ( "cannot set room name for DM" )
}
2025-10-27 16:20:47 +02:00
err = wa . Client . SetGroupName ( ctx , portalJID , msg . Content . Name )
2025-08-28 02:08:25 +02:00
if err != nil {
return false , err
}
msg . Portal . Name = msg . Content . Name
msg . Portal . NameSet = true
return true , nil
}
func ( wa * WhatsAppClient ) HandleMatrixRoomTopic ( ctx context . Context , msg * bridgev2 . MatrixRoomTopic ) ( bool , error ) {
portalJID , err := waid . ParsePortalID ( msg . Portal . ID )
if err != nil {
return false , err
}
if msg . Portal . RoomType == database . RoomTypeDM {
return false , fmt . Errorf ( "cannot set room topic for DM" )
}
newID := wa . Client . GenerateMessageID ( )
oldID := msg . Portal . Metadata . ( * waid . PortalMetadata ) . TopicID
2025-10-27 16:20:47 +02:00
err = wa . Client . SetGroupTopic ( ctx , portalJID , oldID , newID , msg . Content . Topic )
2025-08-28 02:08:25 +02:00
if err != nil {
return false , err
}
msg . Portal . Topic = msg . Content . Topic
msg . Portal . TopicSet = true
msg . Portal . Metadata . ( * waid . PortalMetadata ) . TopicID = newID
return true , nil
}
func ( wa * WhatsAppClient ) HandleMatrixRoomAvatar ( ctx context . Context , msg * bridgev2 . MatrixRoomAvatar ) ( bool , error ) {
portalJID , err := waid . ParsePortalID ( msg . Portal . ID )
if err != nil {
return false , err
}
if msg . Portal . RoomType == database . RoomTypeDM {
return false , fmt . Errorf ( "cannot set room avatar for DM" )
}
var data [ ] byte
2025-12-30 23:01:00 +02:00
if msg . Content . URL != "" {
data , err = msg . Portal . Bridge . Bot . DownloadMedia ( ctx , msg . Content . URL , nil )
2025-08-28 02:08:25 +02:00
if err != nil {
return false , fmt . Errorf ( "failed to download avatar: %w" , err )
}
data , err = convertRoomAvatar ( data )
if err != nil {
return false , err
}
}
2025-10-27 16:20:47 +02:00
avatarID , err := wa . Client . SetGroupPhoto ( ctx , portalJID , data )
2025-08-28 02:08:25 +02:00
if err != nil {
return false , err
}
msg . Portal . AvatarMXC = msg . Content . URL
if data == nil {
msg . Portal . AvatarHash = [ 32 ] byte { }
msg . Portal . AvatarID = "remove"
} else {
msg . Portal . AvatarHash = sha256 . Sum256 ( data )
msg . Portal . AvatarID = networkid . AvatarID ( avatarID )
}
msg . Portal . AvatarSet = true
return true , nil
}
const avatarMaxSize = 720
const avatarMinSize = 190
func convertRoomAvatar ( data [ ] byte ) ( [ ] byte , error ) {
cfg , imageType , err := image . DecodeConfig ( bytes . NewReader ( data ) )
if err != nil {
return nil , fmt . Errorf ( "failed to decode avatar: %w" , err )
}
width , height := cfg . Width , cfg . Height
isCorrectSize := width == height && avatarMinSize < width && width < avatarMaxSize
if isCorrectSize && imageType == "jpeg" {
return data , nil
} else if len ( data ) > 10 * 1024 * 1024 || width > 12000 || height > 12000 {
return nil , fmt . Errorf ( "avatar is too large for re-encoding" )
}
img , _ , err := image . Decode ( bytes . NewReader ( data ) )
if err != nil {
return nil , fmt . Errorf ( "failed to decode avatar: %w" , err )
}
if ! isCorrectSize {
var squareCrop image . Rectangle
var dstSize int
if width > height {
dstSize = max ( avatarMinSize , min ( height , avatarMaxSize ) )
offset := ( width - height ) / 2
squareCrop = image . Rect ( offset , 0 , width - offset , height )
} else {
dstSize = max ( avatarMinSize , min ( width , avatarMaxSize ) )
offset := ( height - width ) / 2
squareCrop = image . Rect ( 0 , offset , width , height - offset )
}
cropped := image . NewRGBA ( image . Rect ( 0 , 0 , dstSize , dstSize ) )
draw . BiLinear . Scale ( cropped , cropped . Rect , img , squareCrop , draw . Src , nil )
img = cropped
}
var buf bytes . Buffer
err = jpeg . Encode ( & buf , img , & jpeg . Options { Quality : jpeg . DefaultQuality } )
if err != nil {
return nil , fmt . Errorf ( "failed to re-encode avatar: %w" , err )
}
return buf . Bytes ( ) , nil
}
2025-10-02 15:14:19 +03:00
func ( wa * WhatsAppClient ) HandleMute ( ctx context . Context , msg * bridgev2 . MatrixMute ) error {
chatJID , err := waid . ParsePortalID ( msg . Portal . ID )
if err != nil {
return err
}
mutedUntil := msg . Content . GetMutedUntilTime ( )
muted := mutedUntil . After ( time . Now ( ) )
muteTS := ptr . Ptr ( mutedUntil . UnixMilli ( ) )
if ! muted || mutedUntil == event . MutedForever {
muteTS = nil
}
return wa . Client . SendAppState ( ctx , appstate . BuildMuteAbs ( chatJID , muted , muteTS ) )
}
func ( wa * WhatsAppClient ) HandleRoomTag ( ctx context . Context , msg * bridgev2 . MatrixRoomTag ) error {
chatJID , err := waid . ParsePortalID ( msg . Portal . ID )
if err != nil {
return err
}
_ , isFavorite := msg . Content . Tags [ event . RoomTagFavourite ]
return wa . Client . SendAppState ( ctx , appstate . BuildPin ( chatJID , isFavorite ) )
}
2025-10-17 07:27:05 +08:00
func ( wa * WhatsAppClient ) getLastMessageInfo ( ctx context . Context , chatJID types . JID , portalKey networkid . PortalKey ) ( time . Time , * waCommon . MessageKey , error ) {
msgs , err := wa . Main . Bridge . DB . Message . GetLastNInPortal ( ctx , portalKey , 1 )
2025-10-02 15:14:19 +03:00
if err != nil {
2025-10-17 07:27:05 +08:00
return time . Time { } , nil , fmt . Errorf ( "failed to get last message in portal: %w" , err )
2025-10-02 15:14:19 +03:00
}
var lastTS time . Time
var lastKey * waCommon . MessageKey
if len ( msgs ) == 1 {
lastTS = msgs [ 0 ] . Timestamp
parsed , _ := waid . ParseMessageID ( msgs [ 0 ] . ID )
if parsed != nil {
fromMe := parsed . Sender . ToNonAD ( ) == wa . JID . ToNonAD ( ) || parsed . Sender . ToNonAD ( ) == wa . GetStore ( ) . GetLID ( ) . ToNonAD ( )
var participant * string
if chatJID . Server == types . GroupServer {
participant = ptr . Ptr ( parsed . Sender . String ( ) )
}
lastKey = & waCommon . MessageKey {
RemoteJID : ptr . Ptr ( chatJID . String ( ) ) ,
FromMe : & fromMe ,
ID : & parsed . ID ,
Participant : participant ,
}
}
}
2025-10-17 07:27:05 +08:00
return lastTS , lastKey , nil
}
func ( wa * WhatsAppClient ) HandleMarkedUnread ( ctx context . Context , msg * bridgev2 . MatrixMarkedUnread ) error {
chatJID , err := waid . ParsePortalID ( msg . Portal . ID )
if err != nil {
return err
}
lastTS , lastKey , err := wa . getLastMessageInfo ( ctx , chatJID , msg . Portal . PortalKey )
if err != nil {
return err
}
2025-10-02 15:14:19 +03:00
return wa . Client . SendAppState ( ctx , appstate . BuildMarkChatAsRead ( chatJID , msg . Content . Unread , lastTS , lastKey ) )
}
2025-10-17 07:27:05 +08:00
func ( wa * WhatsAppClient ) HandleMatrixDeleteChat ( ctx context . Context , msg * bridgev2 . MatrixDeleteChat ) error {
chatJID , err := waid . ParsePortalID ( msg . Portal . ID )
if err != nil {
return err
}
2025-12-18 14:35:35 +02:00
if chatJID . Server == types . GroupServer {
memberInfo , err := wa . Main . Bridge . Matrix . GetMemberInfo ( ctx , msg . Portal . MXID , wa . UserLogin . UserMXID )
if err != nil {
return fmt . Errorf ( "failed to get own member info: %w" , err )
} else if memberInfo . Membership == event . MembershipJoin {
err = wa . Client . LeaveGroup ( ctx , chatJID )
if err != nil {
// TODO ignore errors saying you already left the group?
return fmt . Errorf ( "failed to leave group before deleting chat: %w" , err )
}
}
}
2025-10-17 07:27:05 +08:00
lastTS , lastKey , err := wa . getLastMessageInfo ( ctx , chatJID , msg . Portal . PortalKey )
if err != nil {
return err
}
2026-01-26 15:14:09 +02:00
return wa . Client . SendAppState ( ctx , appstate . BuildDeleteChat ( chatJID , lastTS , lastKey , true ) )
2025-10-17 07:27:05 +08:00
}