1
0
Fork 0
mirror of https://github.com/mautrix/whatsapp.git synced 2026-05-15 10:16:52 -04:00
mautrix-whatsapp/pkg/connector/handlematrix.go
2026-01-26 15:14:09 +02:00

667 lines
23 KiB
Go

package connector
import (
"bytes"
"context"
"crypto/sha256"
"errors"
"fmt"
"image"
"image/jpeg"
"strings"
"time"
"github.com/rs/zerolog"
"go.mau.fi/util/ptr"
"go.mau.fi/util/variationselector"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/appstate"
"go.mau.fi/whatsmeow/proto/waCommon"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
"golang.org/x/image/draw"
"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/event"
"go.mau.fi/mautrix-whatsapp/pkg/msgconv"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
var (
_ 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)
_ bridgev2.RoomNameHandlingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.RoomTopicHandlingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.RoomAvatarHandlingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.MuteHandlingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.TagHandlingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.MarkedUnreadHandlingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.DeleteChatHandlingNetworkAPI = (*WhatsAppClient)(nil)
)
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)
}
resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
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)
}
return wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
}
func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) {
waMsg, req, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, msg.Event, msg.Content, msg.ReplyTo, msg.ThreadRoot, msg.Portal)
if err != nil {
return nil, fmt.Errorf("failed to convert message: %w", err)
}
return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req)
}
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)
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{}
}
if strings.HasPrefix(string(msg.InputTransactionID), whatsmeow.WebMessageIDPrefix) {
req.ID = types.MessageID(msg.InputTransactionID)
} else {
req.ID = wa.Client.GenerateMessageID()
}
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return nil, err
}
if chatJID == types.StatusBroadcastJID && wa.Main.Config.DisableStatusBroadcastSend {
return nil, ErrBroadcastSendDisabled
}
wrappedMsgID := waid.MakeMessageID(chatJID, wa.JID, req.ID)
wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID)
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2))
resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req)
if err != nil {
return nil, err
}
var pickedMessageID networkid.MessageID
if resp.Sender == wa.GetStore().GetLID() && chatJID.Server != types.DefaultUserServer {
pickedMessageID = wrappedMsgID2
msg.RemovePending(networkid.TransactionID(wrappedMsgID))
} else {
pickedMessageID = wrappedMsgID
msg.RemovePending(networkid.TransactionID(wrappedMsgID2))
}
return &bridgev2.MatrixMessageResponse{
DB: &database.Message{
ID: pickedMessageID,
SenderID: waid.MakeUserID(resp.Sender),
Timestamp: resp.Timestamp,
Metadata: &waid.MessageMetadata{
SenderDeviceID: wa.JID.Device,
},
},
StreamOrder: resp.Timestamp.Unix(),
RemovePending: networkid.TransactionID(pickedMessageID),
}, nil
}
func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return bridgev2.MatrixReactionPreResponse{}, fmt.Errorf("failed to parse portal ID: %w", err)
} else if portalJID == types.StatusBroadcastJID {
return bridgev2.MatrixReactionPreResponse{}, ErrBroadcastReactionUnsupported
}
sender := wa.JID
if portalJID.Server == types.HiddenUserServer ||
msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup ||
msg.Portal.Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID {
sender = wa.GetStore().GetLID()
}
return bridgev2.MatrixReactionPreResponse{
SenderID: waid.MakeUserID(sender),
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 {
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
}
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return nil, fmt.Errorf("failed to parse portal ID: %w", err)
}
reactionMsg := &waE2E.Message{
ReactionMessage: &waE2E.ReactionMessage{
Key: wa.messageIDToKey(messageID),
Text: proto.String(msg.PreHandleResp.Emoji),
SenderTimestampMS: proto.Int64(msg.Event.Timestamp),
},
}
var req whatsmeow.SendRequestExtra
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(wa.Client, messageID), reactionMsg.ReactionMessage)
if err != nil {
return nil, fmt.Errorf("failed to encrypt reaction: %w", err)
}
reactionMsg.ReactionMessage = nil
req.Meta = &types.MsgMetaInfo{
DeprecatedLIDSession: ptr.Ptr(false),
}
}
resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, req)
zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response")
return &database.Reaction{
Metadata: &waid.ReactionMetadata{
SenderDeviceID: wa.JID.Device,
},
}, err
}
func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
messageID, err := waid.ParseMessageID(msg.TargetReaction.MessageID)
if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err)
}
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return fmt.Errorf("failed to parse portal ID: %w", err)
}
reactionMsg := &waE2E.Message{
ReactionMessage: &waE2E.ReactionMessage{
Key: wa.messageIDToKey(messageID),
Text: proto.String(""),
SenderTimestampMS: proto.Int64(msg.Event.Timestamp),
},
}
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)
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)
var editID types.MessageID
if strings.HasPrefix(string(edit.InputTransactionID), whatsmeow.WebMessageIDPrefix) {
editID = types.MessageID(edit.InputTransactionID)
} else {
editID = wa.Client.GenerateMessageID()
}
messageID, err := waid.ParseMessageID(edit.EditTarget.ID)
if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err)
}
portalJID, err := waid.ParsePortalID(edit.Portal.ID)
if err != nil {
return fmt.Errorf("failed to parse portal ID: %w", err)
}
waMsg, _, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, edit.Event, edit.Content, nil, nil, edit.Portal)
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)
}
//wrappedMsgID := waid.MakeMessageID(portalJID, wa.JID, messageID)
//edit.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
resp, err := wa.Client.SendMessage(ctx, portalJID, convertedEdit, whatsmeow.SendRequestExtra{
ID: editID,
})
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 {
return fmt.Errorf("failed to parse target message ID: %w", err)
}
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return fmt.Errorf("failed to parse portal ID: %w", err)
}
revokeMessage := wa.Client.BuildRevoke(messageID.Chat, messageID.Sender, messageID.ID)
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)
log.Trace().Any("response", resp).Msg("WhatsApp delete response")
return err
}
func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error {
if !receipt.ReadUpTo.After(receipt.LastRead) {
return nil
}
if receipt.LastRead.IsZero() {
receipt.LastRead = receipt.ReadUpTo.Add(-5 * time.Second)
}
portalJID, err := waid.ParsePortalID(receipt.Portal.ID)
if err != nil {
return fmt.Errorf("failed to parse portal ID: %w", err)
}
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
}
if parsed.Sender.User == wa.GetStore().GetLID().User || parsed.Sender.User == wa.JID.User {
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 {
err = wa.Client.MarkRead(ctx, ids, receipt.Receipt.Timestamp, portalJID, messageSender)
if err != nil {
log.Err(err).Strs("ids", ids).Msg("Failed to mark messages as read")
}
}
return err
}
func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) error {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
}
var chatPresence types.ChatPresence
var mediaPresence types.ChatPresenceMedia
if msg.IsTyping {
chatPresence = types.ChatPresenceComposing
} else {
chatPresence = types.ChatPresencePaused
}
switch msg.Type {
case bridgev2.TypingTypeText:
mediaPresence = types.ChatPresenceMediaText
case bridgev2.TypingTypeRecordingMedia:
mediaPresence = types.ChatPresenceMediaAudio
case bridgev2.TypingTypeUploadingMedia:
return nil
}
if wa.Main.Config.SendPresenceOnTyping {
err = wa.updatePresence(ctx, types.PresenceAvailable)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence on typing")
}
}
return wa.Client.SendChatPresence(ctx, portalJID, chatPresence, mediaPresence)
}
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)
err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS)
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
}
func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return nil, err
}
if msg.Portal.RoomType == database.RoomTypeDM {
switch msg.Type {
case bridgev2.Invite:
return nil, fmt.Errorf("cannot invite additional user to dm")
default:
return nil, nil
}
}
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:
return nil, nil
}
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 {
return nil, fmt.Errorf("failed to get ghost for user: %w", err)
}
changes[0] = waid.ParseUserID(ghost.ID)
default:
return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target)
}
resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action)
if err != nil {
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)
}
zerolog.Ctx(ctx).Debug().
Any("change_response", resp).
Msg("Handled membership change")
return &bridgev2.MatrixMembershipResult{RedirectTo: waid.MakeUserID(resp[0].JID)}, nil
}
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")
}
err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name)
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
err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic)
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
if msg.Content.URL != "" {
data, err = msg.Portal.Bridge.Bot.DownloadMedia(ctx, msg.Content.URL, nil)
if err != nil {
return false, fmt.Errorf("failed to download avatar: %w", err)
}
data, err = convertRoomAvatar(data)
if err != nil {
return false, err
}
}
avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data)
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
}
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))
}
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)
if err != nil {
return time.Time{}, nil, fmt.Errorf("failed to get last message in portal: %w", err)
}
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,
}
}
}
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
}
return wa.Client.SendAppState(ctx, appstate.BuildMarkChatAsRead(chatJID, msg.Content.Unread, lastTS, lastKey))
}
func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
}
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)
}
}
}
lastTS, lastKey, err := wa.getLastMessageInfo(ctx, chatJID, msg.Portal.PortalKey)
if err != nil {
return err
}
return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true))
}