From 5a363d37788c6bd2579548233dbf8db9e5bd75c7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 1 May 2026 14:09:03 +0300 Subject: [PATCH] handlematrix: add support for member events --- go.mod | 2 +- go.sum | 4 +- pkg/connector/handlematrix.go | 144 +++++++++++++++++++++++++++++++++- 3 files changed, 146 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index cb5a46e9..d13aaca4 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( golang.org/x/sync v0.20.0 golang.org/x/tools v0.44.0 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.27.1-0.20260430124810-125ac2c48014 + maunium.net/go/mautrix v0.27.1-0.20260430160443-60db160fa51d rsc.io/qr v0.2.0 ) diff --git a/go.sum b/go.sum index e3dea981..42b5524e 100644 --- a/go.sum +++ b/go.sum @@ -236,7 +236,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.27.1-0.20260430124810-125ac2c48014 h1:KwXGBWwUHYJKVTYWgbZEFcaM6uYLMvfjzHJg/TLwvKc= -maunium.net/go/mautrix v0.27.1-0.20260430124810-125ac2c48014/go.mod h1:4fZ0M0xB5ZtueQI65RilX28J/3794BeK+LaCg4U61Jk= +maunium.net/go/mautrix v0.27.1-0.20260430160443-60db160fa51d h1:kOxWOSl5sYsTBPgGeeEufHBNGRjlPXppmDAs8G0XIkI= +maunium.net/go/mautrix v0.27.1-0.20260430160443-60db160fa51d/go.mod h1:4fZ0M0xB5ZtueQI65RilX28J/3794BeK+LaCg4U61Jk= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index 8fe05fdf..730a3846 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -80,6 +80,7 @@ var ( _ bridgev2.DeleteChatHandlingNetworkAPI = (*TelegramClient)(nil) _ bridgev2.RoomNameHandlingNetworkAPI = (*TelegramClient)(nil) _ bridgev2.RoomAvatarHandlingNetworkAPI = (*TelegramClient)(nil) + _ bridgev2.MembershipHandlingNetworkAPI = (*TelegramClient)(nil) ) func getMediaFilename(content *event.MessageEventContent) (filename string) { @@ -382,7 +383,10 @@ func (tc *TelegramClient) transferMediaToTelegram(ctx context.Context, content * }, nil } -func (tc *TelegramClient) humaniseSendError(err error) bridgev2.MessageStatus { +func (tc *TelegramClient) humaniseSendError(err error) error { + if err == nil { + return nil + } status := bridgev2.WrapErrorInStatus(err). WithErrorReason(event.MessageStatusNetworkError). WithMessage(humanise.Error(err)) @@ -1296,3 +1300,141 @@ func (tc *TelegramClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridg return false, fmt.Errorf("unsupported peer type %s for changing room avatar", peerType) } } + +func wrapUnsupportedError(err error) bridgev2.MessageStatus { + return bridgev2.WrapErrorInStatus(err). + WithErrorReason(event.MessageStatusUnsupported). + WithIsCertain(true). + WithSendNotice(false) +} + +func (tc *TelegramClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) { + if (msg.Type.IsSelf && msg.OrigSender != nil) || msg.Type == bridgev2.ProfileChange { + return nil, nil + } + chatPeerType, chatID, _, err := ids.ParsePortalID(msg.Portal.ID) + if err != nil { + return nil, fmt.Errorf("failed to parse portal ID: %w", err) + } + var inputChannel *tg.InputChannel + switch chatPeerType { + case ids.PeerTypeUser: + return nil, nil + case ids.PeerTypeChannel: + if accessHash, err := tc.ScopedStore.GetAccessHash(ctx, ids.PeerTypeChannel, chatID); err != nil { + return nil, fmt.Errorf("failed to get access hash for channel: %w", err) + } else { + inputChannel = &tg.InputChannel{ChannelID: chatID, AccessHash: accessHash} + } + case ids.PeerTypeChat: + // ok + default: + return nil, wrapUnsupportedError(fmt.Errorf("unsupported chat peer type %s for membership changes", chatPeerType)) + } + var targetPeerType ids.PeerType + var targetUserID int64 + switch target := msg.Target.(type) { + case *bridgev2.UserLogin: + targetPeerType = ids.PeerTypeUser + targetUserID, err = ids.ParseUserLoginID(target.ID) + case *bridgev2.Ghost: + targetPeerType, targetUserID, err = ids.ParseUserID(target.ID) + default: + return nil, wrapUnsupportedError(fmt.Errorf("unknown membership target type %T", msg.Target)) + } + if err != nil { + return nil, fmt.Errorf("failed to parse target login ID: %w", err) + } + targetAccessHash, err := tc.ScopedStore.GetAccessHash(ctx, targetPeerType, targetUserID) + if err != nil { + return nil, fmt.Errorf("failed to get access hash for target: %w", err) + } + var targetInputPeer tg.InputPeerClass + var targetInputUser *tg.InputUser + switch targetPeerType { + case ids.PeerTypeUser: + targetInputPeer = &tg.InputPeerUser{UserID: targetUserID, AccessHash: targetAccessHash} + targetInputUser = &tg.InputUser{UserID: targetUserID, AccessHash: targetAccessHash} + case ids.PeerTypeChannel: + targetInputPeer = &tg.InputPeerChannel{ChannelID: targetUserID, AccessHash: targetAccessHash} + default: + return nil, wrapUnsupportedError(fmt.Errorf("unsupported target peer type %s for membership changes", targetPeerType)) + } + if chatPeerType == ids.PeerTypeChannel { + switch msg.Type { + case bridgev2.Kick, bridgev2.RejectKnock, bridgev2.RevokeInvite, bridgev2.BanLeft, bridgev2.BanJoined, bridgev2.BanInvited, bridgev2.BanKnocked: + _, err = tc.client.API().ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{ + Channel: inputChannel, + Participant: targetInputPeer, + BannedRights: tg.ChatBannedRights{ViewMessages: true}, + }) + if err != nil { + err = tc.humaniseSendError(err) + } else if msg.Type == bridgev2.Kick { + // Reset permissions to default (telegram doesn't have a dedicated kick method) + _, err = tc.client.API().ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{ + Channel: inputChannel, + Participant: targetInputPeer, + BannedRights: tg.ChatBannedRights{}, + }) + if err != nil { + err = tc.humaniseSendError(err) + return nil, fmt.Errorf("failed to unban user to emulate kick: %w", err) + } + } + case bridgev2.Unban: + _, err = tc.client.API().ChannelsEditBanned(ctx, &tg.ChannelsEditBannedRequest{ + Channel: inputChannel, + Participant: targetInputPeer, + BannedRights: tg.ChatBannedRights{}, + }) + err = tc.humaniseSendError(err) + case bridgev2.Invite, bridgev2.AcceptKnock: + if targetInputUser == nil { + return nil, wrapUnsupportedError(fmt.Errorf("can't invite non-user peer type %s", targetPeerType)) + } + _, err = tc.client.API().ChannelsInviteToChannel(ctx, &tg.ChannelsInviteToChannelRequest{ + Channel: inputChannel, + Users: []tg.InputUserClass{targetInputUser}, + }) + err = tc.humaniseSendError(err) + case bridgev2.Join, bridgev2.Knock: + _, err = tc.client.API().ChannelsJoinChannel(ctx, inputChannel) + err = tc.humaniseSendError(err) + case bridgev2.Leave, bridgev2.RetractKnock, bridgev2.RejectInvite: + _, err = tc.client.API().ChannelsLeaveChannel(ctx, inputChannel) + err = tc.humaniseSendError(err) + default: + err = wrapUnsupportedError(fmt.Errorf("unsupported channel membership change type %s -> %s", msg.Type.From, msg.Type.To)) + } + } else { + switch msg.Type { + case bridgev2.Kick, bridgev2.RejectKnock, bridgev2.RevokeInvite, bridgev2.BanLeft, bridgev2.BanJoined, bridgev2.BanInvited, bridgev2.BanKnocked, + bridgev2.Leave, bridgev2.RetractKnock, bridgev2.RejectInvite: + _, err = tc.client.API().MessagesDeleteChatUser(ctx, &tg.MessagesDeleteChatUserRequest{ + ChatID: chatID, + UserID: targetInputUser, + }) + err = tc.humaniseSendError(err) + case bridgev2.Invite, bridgev2.AcceptKnock: + if targetInputUser == nil { + return nil, wrapUnsupportedError(fmt.Errorf("can't invite non-user peer type %s", targetPeerType)) + } + _, err = tc.client.API().MessagesAddChatUser(ctx, &tg.MessagesAddChatUserRequest{ + ChatID: chatID, + UserID: targetInputUser, + FwdLimit: 50, + }) + err = tc.humaniseSendError(err) + case bridgev2.Join, bridgev2.Knock: + // TODO could maybe join if there's a saved invite link in the bridge db? + fallthrough + case bridgev2.Unban: + // There's no way to unban in minigroups + fallthrough + default: + err = wrapUnsupportedError(fmt.Errorf("unsupported minigroup membership change type %s -> %s", msg.Type.From, msg.Type.To)) + } + } + return nil, err +}