From 70e59be4d328058d8b02bde1095cea547b92483d Mon Sep 17 00:00:00 2001 From: Ping Chen Date: Sun, 25 Jan 2026 09:53:48 +0900 Subject: [PATCH 1/2] handlematrix: support adding/removing participant --- pkg/connector/capabilities.go | 8 +++- pkg/connector/handlematrix.go | 66 +++++++++++++++++++++++++++++++++ pkg/connector/handlewhatsapp.go | 35 +++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index 1256e9c..2817d54 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -55,7 +55,7 @@ func (m *MetaConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities { } func (m *MetaConnector) GetBridgeInfoVersion() (info, caps int) { - return 1, 11 + return 1, 12 } const MaxTextLength = 20000 @@ -71,7 +71,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel { } func capID() string { - base := "fi.mau.meta.capabilities.2025_11_25" + base := "fi.mau.meta.capabilities.2026_01_25" if ffmpeg.Supported() { return base + "+ffmpeg" } @@ -161,6 +161,10 @@ var metaCaps = &event.RoomFeatures{ TypingNotifications: true, //LocationMessage: event.CapLevelPartialSupport, DeleteChat: true, + MemberActions: map[event.MemberAction]event.CapabilitySupportLevel{ + event.MemberActionInvite: event.CapLevelFullySupported, + event.MemberActionKick: event.CapLevelFullySupported, + }, } var metaCapsWithThreads *event.RoomFeatures diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index f58c270..8c7c964 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -750,3 +750,69 @@ func (m *MetaClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.M // TODO update portal metadata return true, nil } + +func (m *MetaClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) { + if msg.Portal.RoomType == database.RoomTypeDM { + return nil, errors.New("cannot change members for DM") + } + + var targetID int64 + switch target := msg.Target.(type) { + case *bridgev2.Ghost: + targetID = metaid.ParseUserID(target.ID) + case *bridgev2.UserLogin: + targetID = metaid.ParseUserLoginID(target.ID) + default: + return nil, fmt.Errorf("unknown membership target type %T", target) + } + if targetID == 0 { + return nil, fmt.Errorf("invalid target user ID") + } + + portalMeta := msg.Portal.Metadata.(*metaid.PortalMetadata) + if portalMeta.ThreadType == table.ENCRYPTED_OVER_WA_GROUP { + portalJID := portalMeta.JID(msg.Portal.ID) + targetJID := waTypes.NewJID(strconv.FormatInt(targetID, 10), waTypes.MessengerServer) + var action whatsmeow.ParticipantChange + switch msg.Type { + case bridgev2.Invite: + action = whatsmeow.ParticipantChangeAdd + case bridgev2.Kick: + action = whatsmeow.ParticipantChangeRemove + default: + return nil, nil + } + resp, err := m.E2EEClient.UpdateGroupParticipants(ctx, portalJID, []waTypes.JID{targetJID}, 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) + } + return &bridgev2.MatrixMembershipResult{RedirectTo: metaid.MakeWAUserID(resp[0].JID)}, nil + } + + threadID := metaid.ParseFBPortalID(msg.Portal.ID) + var task socket.Task + switch msg.Type { + case bridgev2.Invite: + task = &socket.AddParticipantsTask{ + ThreadKey: threadID, + ContactIDs: []int64{targetID}, + SyncGroup: 1, + } + case bridgev2.Kick: + task = &socket.RemoveParticipantTask{ + ThreadID: threadID, + ContactID: targetID, + } + default: + return nil, nil + } + _, err := m.Client.ExecuteTasks(ctx, task) + if err != nil { + return nil, err + } + return &bridgev2.MatrixMembershipResult{}, nil +} diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 3a842d6..a898088 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -12,6 +12,7 @@ import ( "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/simplevent" "maunium.net/go/mautrix/bridgev2/status" + "maunium.net/go/mautrix/event" "go.mau.fi/mautrix-meta/pkg/messagix/types" "go.mau.fi/mautrix-meta/pkg/metaid" @@ -133,6 +134,40 @@ func (m *MetaClient) e2eeEventHandler(rawEvt any) bool { Message: evt.PermanentDisconnectDescription(), } m.UserLogin.BridgeState.Send(m.waState) + case *events.GroupInfo: + portalKey := m.makeWAPortalKey(evt.JID) + memberChanges := &bridgev2.ChatMemberList{ + MemberMap: make(map[networkid.UserID]bridgev2.ChatMember), + } + for _, userID := range evt.Join { + memberChanges.MemberMap[metaid.MakeWAUserID(userID)] = bridgev2.ChatMember{ + EventSender: m.makeWAEventSender(userID), + Membership: event.MembershipJoin, + } + } + for _, userID := range evt.Leave { + memberChanges.MemberMap[metaid.MakeWAUserID(userID)] = bridgev2.ChatMember{ + EventSender: m.makeWAEventSender(userID), + Membership: event.MembershipLeave, + PrevMembership: event.MembershipJoin, + } + } + if len(memberChanges.MemberMap) > 0 { + eventMeta := simplevent.EventMeta{ + Type: bridgev2.RemoteEventChatInfoChange, + PortalKey: portalKey, + Timestamp: evt.Timestamp, + } + if evt.Sender != nil { + eventMeta.Sender = m.makeWAEventSender(*evt.Sender) + } + m.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{ + EventMeta: eventMeta, + ChatInfoChange: &bridgev2.ChatInfoChange{ + MemberChanges: memberChanges, + }, + }) + } default: log.Debug().Type("event_type", rawEvt).Msg("Unhandled WhatsApp event") } From 4b2cdf87290130c943ece818e1d7f65090fdc02d Mon Sep 17 00:00:00 2001 From: Ping Chen Date: Tue, 27 Jan 2026 15:01:51 +0900 Subject: [PATCH 2/2] assign MemberActions to group, use MemberMap.Set --- pkg/connector/capabilities.go | 17 ++++++++++++----- pkg/connector/handlewhatsapp.go | 8 ++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index 2817d54..b1f8ba2 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -161,14 +161,11 @@ var metaCaps = &event.RoomFeatures{ TypingNotifications: true, //LocationMessage: event.CapLevelPartialSupport, DeleteChat: true, - MemberActions: map[event.MemberAction]event.CapabilitySupportLevel{ - event.MemberActionInvite: event.CapLevelFullySupported, - event.MemberActionKick: event.CapLevelFullySupported, - }, } var metaCapsWithThreads *event.RoomFeatures var metaCapsWithE2E *event.RoomFeatures +var metaCapsWithE2EGroup *event.RoomFeatures var igCaps *event.RoomFeatures var igCapsGroup *event.RoomFeatures var metaCapsGroup *event.RoomFeatures @@ -190,6 +187,12 @@ func init() { delete(metaCapsWithE2E.File[event.MsgVideo].MimeTypes, "video/webm") delete(metaCapsWithE2E.File[event.MsgVideo].MimeTypes, "video/ogg") metaCapsWithE2E.DeleteChat = false + metaCapsWithE2EGroup = metaCapsWithE2E.Clone() + metaCapsWithE2EGroup.ID += "+group" + metaCapsWithE2EGroup.MemberActions = map[event.MemberAction]event.CapabilitySupportLevel{ + event.MemberActionInvite: event.CapLevelFullySupported, + event.MemberActionKick: event.CapLevelFullySupported, + } metaCapsGroup = metaCaps.Clone() metaCapsGroup.ID += "+group" @@ -197,6 +200,7 @@ func init() { event.StateRoomName.Type: {Level: event.CapLevelFullySupported}, event.StateRoomAvatar.Type: {Level: event.CapLevelFullySupported}, } + metaCapsGroup.MemberActions = metaCapsWithE2EGroup.MemberActions.Clone() igCaps = metaCaps.Clone() delete(igCaps.File, event.MsgFile) @@ -209,14 +213,17 @@ func init() { igCapsGroup.State = event.StateFeatureMap{ event.StateRoomName.Type: {Level: event.CapLevelFullySupported}, } + igCapsGroup.MemberActions = metaCapsWithE2EGroup.MemberActions.Clone() } func (m *MetaClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *event.RoomFeatures { switch portal.Metadata.(*metaid.PortalMetadata).ThreadType { case table.COMMUNITY_GROUP: return metaCapsWithThreads - case table.ENCRYPTED_OVER_WA_ONE_TO_ONE, table.ENCRYPTED_OVER_WA_GROUP: + case table.ENCRYPTED_OVER_WA_ONE_TO_ONE: return metaCapsWithE2E + case table.ENCRYPTED_OVER_WA_GROUP: + return metaCapsWithE2EGroup } if m.Client.GetPlatform() == types.Instagram || m.Main.Config.Mode == types.Instagram { if portal.RoomType == database.RoomTypeDM { diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index a898088..ae82c08 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -140,17 +140,17 @@ func (m *MetaClient) e2eeEventHandler(rawEvt any) bool { MemberMap: make(map[networkid.UserID]bridgev2.ChatMember), } for _, userID := range evt.Join { - memberChanges.MemberMap[metaid.MakeWAUserID(userID)] = bridgev2.ChatMember{ + memberChanges.MemberMap.Set(bridgev2.ChatMember{ EventSender: m.makeWAEventSender(userID), Membership: event.MembershipJoin, - } + }) } for _, userID := range evt.Leave { - memberChanges.MemberMap[metaid.MakeWAUserID(userID)] = bridgev2.ChatMember{ + memberChanges.MemberMap.Set(bridgev2.ChatMember{ EventSender: m.makeWAEventSender(userID), Membership: event.MembershipLeave, PrevMembership: event.MembershipJoin, - } + }) } if len(memberChanges.MemberMap) > 0 { eventMeta := simplevent.EventMeta{