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