handlematrix: add support for member events

This commit is contained in:
Tulir Asokan
2026-05-01 14:09:03 +03:00
parent e3bb26aee1
commit 5a363d3778
3 changed files with 146 additions and 4 deletions

2
go.mod
View File

@@ -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
)

4
go.sum
View File

@@ -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=

View File

@@ -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
}