diff --git a/go.mod b/go.mod index ac5eeba4..c49fa0ff 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/net v0.27.0 google.golang.org/protobuf v1.34.2 - maunium.net/go/mautrix v0.19.1-0.20240808174455-5edfcff2b7b6 + maunium.net/go/mautrix v0.19.1-0.20240809135320-eb84187368b7 nhooyr.io/websocket v1.8.11 ) diff --git a/go.sum b/go.sum index 11d927a6..4972d7b5 100644 --- a/go.sum +++ b/go.sum @@ -88,7 +88,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.19.1-0.20240808174455-5edfcff2b7b6 h1:OGskR/suV/6OGgURhucS8eH5QRyRzw7Wkpf8exrOakk= -maunium.net/go/mautrix v0.19.1-0.20240808174455-5edfcff2b7b6/go.mod h1:ZWyxoQxRTBxzWIMs0kQCVogZIY0clTu33h102veCT/Q= +maunium.net/go/mautrix v0.19.1-0.20240809135320-eb84187368b7 h1:xAq4d1rW/5WN7szBtxHNY/63EPNdji+h7FXlLGBUfJU= +maunium.net/go/mautrix v0.19.1-0.20240809135320-eb84187368b7/go.mod h1:ZWyxoQxRTBxzWIMs0kQCVogZIY0clTu33h102veCT/Q= nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/pkg/connector/groupinfo.go b/pkg/connector/groupinfo.go index db06b957..5c25942e 100644 --- a/pkg/connector/groupinfo.go +++ b/pkg/connector/groupinfo.go @@ -345,7 +345,9 @@ func (s *SignalClient) catchUpGroup(ctx context.Context, portal *bridgev2.Portal for _, gc := range groupChanges { log.Debug().Uint32("current_rev", gc.GroupChange.Revision).Msg("Processing group change") chatInfoChange := s.groupChangeToChatInfoChange(ctx, gc.GroupChange.Revision, gc.GroupChange) - portal.ProcessChatInfoChange(ctx, s.makeEventSender(gc.GroupChange.SourceACI), s.UserLogin, chatInfoChange, time.UnixMilli(int64(ts))) + if gc.GroupChange.SourceServiceID.Type == libsignalgo.ServiceIDTypeACI { + portal.ProcessChatInfoChange(ctx, s.makeEventSender(gc.GroupChange.SourceServiceID.UUID), s.UserLogin, chatInfoChange, time.UnixMilli(int64(ts))) + } if gc.GroupChange.Revision == toRevision { break } diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 9e8c3532..2b8ca269 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -30,6 +30,7 @@ import ( "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-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/signalid" @@ -344,3 +345,130 @@ func (s *SignalClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2. ModifyDescription: &msg.Content.Topic, }, nil) } + +func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (bool, error) { + var targetIntent bridgev2.MatrixAPI + var targetSignalID uuid.UUID + var err error + if msg.Portal.RoomType == database.RoomTypeDM { + //TODO: this probably needs to revert some changes and clean up the portal on leaves + switch msg.Type { + case bridgev2.Invite: + return false, fmt.Errorf("cannot invite additional user to dm") + default: + return false, nil + } + } + if msg.TargetGhost != nil { + targetIntent = msg.TargetGhost.Intent + targetSignalID, err = signalid.ParseUserID(msg.TargetGhost.ID) + if err != nil { + return false, fmt.Errorf("failed to parse target ghost signal id: %w", err) + } + } else if msg.TargetUserLogin != nil { + targetSignalID, err = signalid.ParseUserLoginID(msg.TargetUserLogin.ID) + if err != nil { + return false, fmt.Errorf("failed to parse target user signal id: %w", err) + } + targetIntent = msg.TargetUserLogin.User.DoublePuppet(ctx) + if targetIntent == nil { + ghost, err := s.Main.Bridge.GetGhostByID(ctx, networkid.UserID(msg.TargetUserLogin.ID)) + if err != nil { + return false, fmt.Errorf("failed to get ghost for user: %w", err) + } + targetIntent = ghost.Intent + } + } + log := zerolog.Ctx(ctx).With(). + Str("From Membership", string(msg.Type.From)). + Str("To Membership", string(msg.Type.To)). + Logger() + gc := &signalmeow.GroupChange{} + role := signalmeow.GroupMember_DEFAULT + if msg.Type.To == event.MembershipInvite || msg.Type == bridgev2.AcceptKnock { + levels, err := msg.Portal.Bridge.Matrix.GetPowerLevels(ctx, msg.Portal.MXID) + if err != nil { + log.Err(err).Msg("Couldn't get power levels") + if levels.GetUserLevel(targetIntent.GetMXID()) >= 50 { + role = signalmeow.GroupMember_ADMINISTRATOR + } + } + } + switch msg.Type { + case bridgev2.AcceptInvite: + gc.PromotePendingMembers = []*signalmeow.PromotePendingMember{{ + ACI: targetSignalID, + }} + case bridgev2.RevokeInvite, bridgev2.RejectInvite: + deletePendingMember := libsignalgo.NewACIServiceID(targetSignalID) + gc.DeletePendingMembers = []*libsignalgo.ServiceID{&deletePendingMember} + case bridgev2.Leave, bridgev2.Kick: + gc.DeleteMembers = []*uuid.UUID{&targetSignalID} + case bridgev2.Invite: + gc.AddMembers = []*signalmeow.AddMember{{ + GroupMember: signalmeow.GroupMember{ + ACI: targetSignalID, + Role: role, + }, + }} + // TODO: joining and knocking requires a way to obtain the invite link + // because the joining/knocking member doesn't have the GroupMasterKey yet + // case bridgev2.Join: + // gc.AddMembers = []*signalmeow.AddMember{{ + // GroupMember: signalmeow.GroupMember{ + // ACI: targetSignalID, + // Role: role, + // }, + // JoinFromInviteLink: true, + // }} + // case bridgev2.Knock: + // gc.AddRequestingMembers = []*signalmeow.RequestingMember{{ + // ACI: targetSignalID, + // Timestamp: uint64(time.Now().UnixMilli()), + // }} + case bridgev2.AcceptKnock: + gc.PromoteRequestingMembers = []*signalmeow.RoleMember{{ + ACI: targetSignalID, + Role: role, + }} + case bridgev2.RetractKnock, bridgev2.RejectKnock: + gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID} + case bridgev2.BanKnocked, bridgev2.BanInvited, bridgev2.BanJoined, bridgev2.BanLeft: + gc.AddBannedMembers = []*signalmeow.BannedMember{{ + ServiceID: libsignalgo.NewACIServiceID(targetSignalID), + Timestamp: uint64(time.Now().UnixMilli()), + }} + switch msg.Type { + case bridgev2.BanJoined: + gc.DeleteMembers = []*uuid.UUID{&targetSignalID} + case bridgev2.BanInvited: + deletePendingMember := libsignalgo.NewACIServiceID(targetSignalID) + gc.DeletePendingMembers = []*libsignalgo.ServiceID{&deletePendingMember} + case bridgev2.BanKnocked: + gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID} + } + case bridgev2.Unban: + unbanUser := libsignalgo.NewACIServiceID(targetSignalID) + gc.DeleteBannedMembers = []*libsignalgo.ServiceID{&unbanUser} + default: + log.Debug().Msg("unsupported membership change") + return false, nil + } + _, groupID, err := signalid.ParsePortalID(msg.Portal.ID) + if err != nil || groupID == "" { + return false, err + } + gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1 + revision, err := s.Client.UpdateGroup(ctx, gc, groupID) + if err != nil { + return false, err + } + if msg.Type == bridgev2.Invite { + err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID) + if err != nil { + return false, err + } + } + msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision + return true, nil +} diff --git a/pkg/signalid/ids.go b/pkg/signalid/ids.go index f99bf9d1..99ef7b3e 100644 --- a/pkg/signalid/ids.go +++ b/pkg/signalid/ids.go @@ -39,6 +39,14 @@ func ParseUserID(userID networkid.UserID) (uuid.UUID, error) { } } +func ParseUserLoginID(userLoginID networkid.UserLoginID) (uuid.UUID, error) { + userID, err := uuid.Parse(string(userLoginID)) + if err != nil { + return uuid.Nil, err + } + return userID, nil +} + func ParseUserIDAsServiceID(userID networkid.UserID) (libsignalgo.ServiceID, error) { return libsignalgo.ServiceIDFromString(string(userID)) } diff --git a/pkg/signalmeow/groups.go b/pkg/signalmeow/groups.go index 0a67ef63..90311940 100644 --- a/pkg/signalmeow/groups.go +++ b/pkg/signalmeow/groups.go @@ -154,7 +154,7 @@ type RequestingMember struct { Timestamp uint64 } -type PromotePendingMembers struct { +type PromotePendingMember struct { ACI uuid.UUID ProfileKey libsignalgo.ProfileKey } @@ -178,7 +178,7 @@ type BannedMember struct { type GroupChange struct { groupMasterKey types.SerializedGroupMasterKey - SourceACI uuid.UUID + SourceServiceID libsignalgo.ServiceID Revision uint32 AddMembers []*AddMember DeleteMembers []*uuid.UUID @@ -186,7 +186,7 @@ type GroupChange struct { ModifyMemberProfileKeys []*ProfileKeyMember AddPendingMembers []*PendingMember DeletePendingMembers []*libsignalgo.ServiceID - PromotePendingMembers []*GroupMember + PromotePendingMembers []*PromotePendingMember ModifyTitle *string ModifyAvatar *string ModifyDisappearingMessagesDuration *uint32 @@ -862,9 +862,9 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange return nil, fmt.Errorf("wrong serviceid kind: expected aci, got pni") } decryptedGroupChange := &GroupChange{ - groupMasterKey: groupMasterKey, - Revision: encryptedActions.Revision, - SourceACI: sourceServiceID.UUID, + groupMasterKey: groupMasterKey, + Revision: encryptedActions.Revision, + SourceServiceID: sourceServiceID, } if encryptedActions.ModifyTitle != nil { @@ -992,7 +992,7 @@ func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange if err != nil { return nil, err } - decryptedGroupChange.PromotePendingMembers = append(decryptedGroupChange.PromotePendingMembers, &GroupMember{ + decryptedGroupChange.PromotePendingMembers = append(decryptedGroupChange.PromotePendingMembers, &PromotePendingMember{ ACI: *aci, ProfileKey: *profileKey, })