Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pkg/connector/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ var whatsappCaps = &event.RoomFeatures{
event.StateRoomName.Type: {Level: event.CapLevelFullySupported},
event.StateRoomAvatar.Type: {Level: event.CapLevelFullySupported},
event.StateTopic.Type: {Level: event.CapLevelFullySupported},
event.StatePinnedEvents.Type: {Level: event.CapLevelFullySupported},
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
},
MemberActions: event.MemberFeatureMap{
Expand Down
12 changes: 7 additions & 5 deletions pkg/connector/chatinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
event.StateRoomName: 0,
event.StateRoomAvatar: 0,
event.StateTopic: 0,
event.StatePinnedEvents: 0,
event.StateBeeperDisappearingTimer: 0,
},
},
Expand Down Expand Up @@ -266,11 +267,12 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
Ban: ptr.Ptr(nobodyPL),
// TODO allow invites if bridge config says to allow them, or maybe if relay mode is enabled?
Events: map[event.Type]int{
event.StateRoomName: metaChangePL,
event.StateRoomAvatar: metaChangePL,
event.StateTopic: metaChangePL,
event.EventReaction: defaultPL,
event.EventRedaction: defaultPL,
event.StateRoomName: metaChangePL,
event.StateRoomAvatar: metaChangePL,
event.StateTopic: metaChangePL,
event.StatePinnedEvents: defaultPL,
event.EventReaction: defaultPL,
event.EventRedaction: defaultPL,

event.StateBeeperDisappearingTimer: metaChangePL,
// TODO always allow poll responses
Expand Down
62 changes: 62 additions & 0 deletions pkg/connector/handlematrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"image"
"image/jpeg"
"slices"
"strings"
"time"

Expand All @@ -25,6 +26,7 @@ import (
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"

"go.mau.fi/mautrix-whatsapp/pkg/msgconv"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
Expand All @@ -46,6 +48,7 @@ var (
_ bridgev2.TagHandlingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.MarkedUnreadHandlingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.DeleteChatHandlingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.PinHandlingNetworkAPI = (*WhatsAppClient)(nil)
)

func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) {
Expand Down Expand Up @@ -603,6 +606,65 @@ func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.Matri
return wa.Client.SendAppState(ctx, appstate.BuildPin(chatJID, isFavorite))
}

func (wa *WhatsAppClient) HandleMatrixPinChange(ctx context.Context, msg *bridgev2.MatrixPinChange) (bool, error) {
log := zerolog.Ctx(ctx)
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
}
var oldPinned []id.EventID
if msg.PrevContent != nil {
oldPinned = msg.PrevContent.Pinned
}
for _, evtID := range msg.Content.Pinned {
if !slices.Contains(oldPinned, evtID) {
if err := wa.sendPinInChat(ctx, chatJID, evtID, waE2E.PinInChatMessage_PIN_FOR_ALL); err != nil {
log.Err(err).Stringer("event_id", evtID).Msg("Failed to send pin to WhatsApp")
}
}
}
for _, evtID := range oldPinned {
if !slices.Contains(msg.Content.Pinned, evtID) {
if err := wa.sendPinInChat(ctx, chatJID, evtID, waE2E.PinInChatMessage_UNPIN_FOR_ALL); err != nil {
log.Err(err).Stringer("event_id", evtID).Msg("Failed to send unpin to WhatsApp")
}
}
}
return false, nil
}

func (wa *WhatsAppClient) sendPinInChat(ctx context.Context, chatJID types.JID, evtID id.EventID, pinType waE2E.PinInChatMessage_Type) error {
msg, err := wa.Main.Bridge.DB.Message.GetPartByMXID(ctx, evtID)
if err != nil {
return fmt.Errorf("failed to get message by MXID: %w", err)
}
if msg == nil {
return fmt.Errorf("message %s not found in database", evtID)
}
parsed, err := waid.ParseMessageID(msg.ID)
if err != nil {
return fmt.Errorf("failed to parse message ID: %w", err)
}
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())
}
_, err = wa.Client.SendMessage(ctx, chatJID, &waE2E.Message{
PinInChatMessage: &waE2E.PinInChatMessage{
Key: &waCommon.MessageKey{
RemoteJID: ptr.Ptr(chatJID.String()),
FromMe: &fromMe,
ID: &parsed.ID,
Participant: participant,
},
Type: pinType.Enum(),
SenderTimestampMS: proto.Int64(time.Now().UnixMilli()),
},
})
return err
}

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 {
Expand Down
75 changes: 75 additions & 0 deletions pkg/connector/handlewhatsapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package connector
import (
"context"
"fmt"
"slices"
"strconv"
"strings"
"time"
Expand All @@ -36,7 +37,9 @@ import (
"maunium.net/go/mautrix/bridgev2/simplevent"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"

"go.mau.fi/mautrix-whatsapp/pkg/msgconv"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)

Expand Down Expand Up @@ -355,6 +358,9 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
return
}
if parsedMessageType == "pin in chat" {
return wa.handleWAPinInChat(ctx, evt)
}
if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
decrypted, err := wa.Client.DecryptReaction(ctx, evt)
if err != nil {
Expand Down Expand Up @@ -816,6 +822,75 @@ func (wa *WhatsAppClient) handleWAPin(evt *events.Pin) bool {
})
}

func (wa *WhatsAppClient) handleWAPinInChat(ctx context.Context, evt *events.Message) bool {
log := zerolog.Ctx(ctx)
pinMsg := evt.Message.GetPinInChatMessage()
if pinMsg == nil || pinMsg.GetKey() == nil {
log.Warn().Msg("Received pin in chat message with no key")
return true
}
isPin := pinMsg.GetType() != waE2E.PinInChatMessage_UNPIN_FOR_ALL
targetMsgID := msgconv.KeyToMessageID(ctx, wa.Client, evt.Info.Chat, evt.Info.Sender, pinMsg.GetKey())
if targetMsgID == "" {
log.Warn().Msg("Failed to determine target message ID for pin in chat")
return true
}
portalKey := wa.makeWAPortalKey(evt.Info.Chat)
portal, err := wa.Main.Bridge.GetPortalByKey(ctx, portalKey)
if err != nil || portal == nil || portal.MXID == "" {
log.Warn().Err(err).Msg("Failed to get portal for pin in chat")
return true
}
targetMsg, err := wa.Main.Bridge.DB.Message.GetFirstPartByID(ctx, wa.UserLogin.ID, targetMsgID)
if err != nil {
log.Warn().Err(err).Msg("Failed to look up target message for pin in chat")
return true
}
if targetMsg == nil {
log.Debug().Str("target_message_id", string(targetMsgID)).Msg("Target message for pin not found in database, ignoring")
return true
}
mx, ok := wa.Main.Bridge.Matrix.(bridgev2.MatrixConnectorWithArbitraryRoomState)
if !ok {
log.Warn().Msg("Matrix connector does not support reading room state, can't update pinned events")
return true
}
var pinned []id.EventID
stateEvt, err := mx.GetStateEvent(ctx, portal.MXID, event.StatePinnedEvents, "")
if err != nil {
log.Debug().Err(err).Msg("Failed to get current pinned events state, assuming empty")
} else if stateEvt != nil {
content, ok := stateEvt.Content.Parsed.(*event.PinnedEventsEventContent)
if ok && content != nil {
pinned = content.Pinned
}
}
if isPin {
if slices.Contains(pinned, targetMsg.MXID) {
return true
}
pinned = append(pinned, targetMsg.MXID)
} else {
idx := slices.Index(pinned, targetMsg.MXID)
if idx < 0 {
return true
}
pinned = slices.Delete(pinned, idx, idx+1)
}
_, err = wa.Main.Bridge.Bot.SendState(ctx, portal.MXID, event.StatePinnedEvents, "", &event.Content{
Parsed: &event.PinnedEventsEventContent{Pinned: pinned},
}, evt.Info.Timestamp)
if err != nil {
log.Err(err).Msg("Failed to update pinned events state")
return false
}
log.Info().
Bool("is_pin", isPin).
Stringer("target_event_id", targetMsg.MXID).
Msg("Updated pinned events in Matrix room")
return true
}

func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt *events.AppStateSyncComplete) {
log := zerolog.Ctx(ctx).With().
Str("patch_name", string(evt.Name)).
Expand Down
2 changes: 2 additions & 0 deletions pkg/connector/wamsgtype.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ func getMessageType(waMsg *waE2E.Message) string {
return "message history bundle"
case waMsg.RequestPhoneNumberMessage != nil:
return "request phone number"
case waMsg.PinInChatMessage != nil:
return "pin in chat"
case waMsg.KeepInChatMessage != nil:
return "keep in chat"
case waMsg.StatusMentionMessage != nil:
Expand Down