From 80e99e878137da65cbfc056c12af88e5b96ca911 Mon Sep 17 00:00:00 2001 From: Astra Date: Tue, 8 Sep 2026 11:42:03 +0100 Subject: [PATCH] Don't 500 on channel sends with an unresolvable @mention A channel post containing an @token that is not a syntactically valid username (too short, leading digit, etc.) made messages.sendMessage return 500 INTERNAL_SERVER_ERROR: mentionedUserIDsFromMessage turned every ResolveUsername error into internalErr(). Skip tokens that fail with ErrUsernameInvalid / ErrUsernameNotOccupied instead, matching real Telegram (the message sends, the client renders the mention and only fails to open it on tap). Only unexpected storage errors still abort. Same fix applied to the bot send path. --- internal/rpc/channels_updates_rpc_test.go | 33 +++++++++++++++++++++++ internal/rpc/messages_bot_no_state.go | 5 +++- internal/rpc/messages_send.go | 16 ++++++++++- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/internal/rpc/channels_updates_rpc_test.go b/internal/rpc/channels_updates_rpc_test.go index b05d0b08..57a60fe5 100644 --- a/internal/rpc/channels_updates_rpc_test.go +++ b/internal/rpc/channels_updates_rpc_test.go @@ -640,6 +640,39 @@ func TestChannelUnreadMentionsRPCUsesMentionState(t *testing.T) { } } +// A channel post containing an @token that is not a syntactically valid +// username (too short, leading digit, etc.) must still send. Real Telegram +// renders it as a mention and only fails when the reader taps it; it never +// rejects the send. Regression for a 500 INTERNAL_SERVER_ERROR. +func TestChannelSendMessageWithUnresolvableMentionSucceeds(t *testing.T) { + ctx := context.Background() + userStore := memory.NewUserStore() + owner, _ := userStore.Create(ctx, domain.User{AccessHash: 9201, Phone: "15550009201", FirstName: "Owner", Username: "owner_badmention"}) + channelStore := memory.NewChannelStore() + channelService := appchannels.NewService(channelStore) + r := New(Config{}, Deps{ + Users: appusers.NewService(userStore), + Channels: channelService, + }, zaptest.NewLogger(t), clock.System) + created, err := channelService.CreateMegagroupFromCreateChat(ctx, owner.ID, domain.CreateChannelRequest{ + Title: "Bad Mention", + Date: 1700009201, + }) + if err != nil { + t.Fatalf("create megagroup: %v", err) + } + peer := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash} + for i, text := range []string{"look at @zio", "hi @2cool and @_x"} { + if _, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{ + Peer: peer, + Message: text, + RandomID: int64(9202001 + i), + }); err != nil { + t.Fatalf("send %q: unexpected error %v", text, err) + } + } +} + func TestChannelDifferenceIncludesExtraForwardSourceChannel(t *testing.T) { channel := domain.Channel{ID: 2000000100, AccessHash: 9010, Title: "Megagroup", Megagroup: true, Date: 1700000000, Pts: 3} source := domain.Channel{ID: 2000000101, AccessHash: 9011, Title: "Source", Broadcast: true, Date: 1700000000} diff --git a/internal/rpc/messages_bot_no_state.go b/internal/rpc/messages_bot_no_state.go index 8cedef6e..ad82ff36 100644 --- a/internal/rpc/messages_bot_no_state.go +++ b/internal/rpc/messages_bot_no_state.go @@ -341,7 +341,10 @@ func (r *Router) mentionedUserIDsFromDomainMessage(ctx context.Context, currentU for _, username := range extractMentionUsernames(message, domain.MaxChannelMentionRecipients-len(out)) { user, found, err := identity.ResolveUsername(ctx, currentUserID, username) if err != nil { - return nil, internalErr() + if mentionResolveFatal(err) { + return nil, internalErr() + } + continue } if found { add(user.ID) diff --git a/internal/rpc/messages_send.go b/internal/rpc/messages_send.go index 51cbc892..ec8d6880 100644 --- a/internal/rpc/messages_send.go +++ b/internal/rpc/messages_send.go @@ -437,7 +437,10 @@ func (r *Router) mentionedUserIDsFromMessage(ctx context.Context, currentUserID for _, username := range extractMentionUsernames(message, domain.MaxChannelMentionRecipients-len(out)) { user, found, err := identity.ResolveUsername(ctx, currentUserID, username) if err != nil { - return nil, internalErr() + if mentionResolveFatal(err) { + return nil, internalErr() + } + continue } if found { add(user.ID) @@ -450,6 +453,17 @@ func (r *Router) mentionedUserIDsFromMessage(ctx context.Context, currentUserID return out, nil } +// mentionResolveFatal reports whether a ResolveUsername error while scanning +// message text for @mentions should abort the send. A syntactically invalid or +// unoccupied @token is not a failure: real Telegram sends the message, renders +// the token as a mention, and only fails to open it when the reader taps it. +// Only an unexpected (storage) error aborts the RPC. +func mentionResolveFatal(err error) bool { + return err != nil && + !errors.Is(err, domain.ErrUsernameInvalid) && + !errors.Is(err, domain.ErrUsernameNotOccupied) +} + func extractMentionUsernames(message string, limit int) []string { if limit <= 0 || message == "" { return nil