mirror of
https://github.com/mautrix/whatsapp.git
synced 2026-05-15 10:16:52 -04:00
667 lines
23 KiB
Go
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))
|
|
}
|