usernames: report reserved names as taken in the check paths too
account.checkUsername / channels.checkUsername / bots.checkUsername said a reserved name was available and only updateUsername rejected it. Add the blocklist check to peerUsernameAvailable (covers account + channel, both backends) and to bots.Service.CheckUsername, so the client shows "username is taken" immediately.
This commit is contained in:
parent
66091ede72
commit
d1108c61f1
7 changed files with 79 additions and 5 deletions
|
|
@ -1007,10 +1007,12 @@ func run(logger *zap.Logger) error {
|
||||||
account.WithLoginEmailVerification(codeStore, loginEmailSender, cfg.AuthCodeTTL, cfg.AuthCodeMaxAttempts, cfg.LoginEmailCodeLength))
|
account.WithLoginEmailVerification(codeStore, loginEmailSender, cfg.AuthCodeTTL, cfg.AuthCodeMaxAttempts, cfg.LoginEmailCodeLength))
|
||||||
}
|
}
|
||||||
accountService := account.NewService(passwordStore, accountOptions...)
|
accountService := account.NewService(passwordStore, accountOptions...)
|
||||||
|
reservedUsernameStore := postgres.NewReservedUsernameStore(pool)
|
||||||
botsService := botsapp.NewService(userStore, botStore, messageStore,
|
botsService := botsapp.NewService(userStore, botStore, messageStore,
|
||||||
botsapp.WithLogger(logger.Named("bots")),
|
botsapp.WithLogger(logger.Named("bots")),
|
||||||
botsapp.WithBlockChecker(contactStore),
|
botsapp.WithBlockChecker(contactStore),
|
||||||
botsapp.WithPublicChannelUsernameResolver(channelStore),
|
botsapp.WithPublicChannelUsernameResolver(channelStore),
|
||||||
|
botsapp.WithReservedUsernames(reservedUsernameStore),
|
||||||
botsapp.WithUserCache(userCache),
|
botsapp.WithUserCache(userCache),
|
||||||
botsapp.WithStickerSetCreator(filesService),
|
botsapp.WithStickerSetCreator(filesService),
|
||||||
botsapp.WithGifCatalogSource(filesService),
|
botsapp.WithGifCatalogSource(filesService),
|
||||||
|
|
@ -1203,7 +1205,6 @@ func run(logger *zap.Logger) error {
|
||||||
// Collectible (NFT) usernames are an optional read model projected at the
|
// Collectible (NFT) usernames are an optional read model projected at the
|
||||||
// protocol edge.
|
// protocol edge.
|
||||||
collectibleUsernameStore := postgres.NewCollectibleUsernameStore(pool)
|
collectibleUsernameStore := postgres.NewCollectibleUsernameStore(pool)
|
||||||
reservedUsernameStore := postgres.NewReservedUsernameStore(pool)
|
|
||||||
usernamesService := usernamesapp.NewService(
|
usernamesService := usernamesapp.NewService(
|
||||||
usernamesapp.WithRegistryStore(collectibleUsernameStore),
|
usernamesapp.WithRegistryStore(collectibleUsernameStore),
|
||||||
usernamesapp.WithCollectibleStore(collectibleUsernameStore),
|
usernamesapp.WithCollectibleStore(collectibleUsernameStore),
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ type Service struct {
|
||||||
messages store.MessageStore
|
messages store.MessageStore
|
||||||
blocker blockChecker
|
blocker blockChecker
|
||||||
channels publicChannelUsernameResolver
|
channels publicChannelUsernameResolver
|
||||||
|
reserved reservedUsernameChecker
|
||||||
stickers stickerSetCreator
|
stickers stickerSetCreator
|
||||||
installer userStickerSetInstaller
|
installer userStickerSetInstaller
|
||||||
aiChat aiChatGenerator
|
aiChat aiChatGenerator
|
||||||
|
|
@ -199,6 +200,21 @@ func WithBotAvatarStore(a botAvatarStore) Option {
|
||||||
|
|
||||||
// WithPublicChannelUsernameResolver 注入公开频道 username 查询能力,用于 bot
|
// WithPublicChannelUsernameResolver 注入公开频道 username 查询能力,用于 bot
|
||||||
// username 预检,避免 bot 与 public channel 产生同名可见入口。
|
// username 预检,避免 bot 与 public channel 产生同名可见入口。
|
||||||
|
// reservedUsernameChecker reports whether a name is on the operator blocklist.
|
||||||
|
type reservedUsernameChecker interface {
|
||||||
|
IsReserved(ctx context.Context, usernameLower string) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithReservedUsernames wires the operator username blocklist so CheckUsername
|
||||||
|
// reports a reserved bot name as taken instead of available.
|
||||||
|
func WithReservedUsernames(c reservedUsernameChecker) Option {
|
||||||
|
return func(s *Service) {
|
||||||
|
if c != nil {
|
||||||
|
s.reserved = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithPublicChannelUsernameResolver(c publicChannelUsernameResolver) Option {
|
func WithPublicChannelUsernameResolver(c publicChannelUsernameResolver) Option {
|
||||||
return func(s *Service) {
|
return func(s *Service) {
|
||||||
if c != nil {
|
if c != nil {
|
||||||
|
|
@ -533,6 +549,13 @@ func (s *Service) CheckUsername(ctx context.Context, ownerUserID int64, username
|
||||||
if !domain.ValidBotUsername(username) {
|
if !domain.ValidBotUsername(username) {
|
||||||
return false, domain.ErrBotUsernameInvalid
|
return false, domain.ErrBotUsernameInvalid
|
||||||
}
|
}
|
||||||
|
if s.reserved != nil {
|
||||||
|
if r, err := s.reserved.IsReserved(ctx, strings.ToLower(username)); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if r {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
if _, found, err := s.users.ByUsername(ctx, username); err != nil {
|
if _, found, err := s.users.ByUsername(ctx, username); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if found {
|
} else if found {
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,9 @@ func (s *ChannelStore) CheckUsername(_ context.Context, userID, channelID int64,
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
usernameLower := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(username, "@")))
|
usernameLower := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(username, "@")))
|
||||||
|
if s.usernameRegistry != nil && s.usernameRegistry.nameReserved(usernameLower) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
for id, channel := range s.channels {
|
for id, channel := range s.channels {
|
||||||
if channel.Deleted || channel.Username == "" {
|
if channel.Deleted || channel.Username == "" {
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,10 @@ func (s *CollectibleUsernameStore) WithReservedUsernames(reserved *ReservedUsern
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CollectibleUsernameStore) nameReservedLocked(usernameLower string) bool {
|
// nameReserved reports whether a name is on the operator blocklist. It touches
|
||||||
if s.reserved == nil {
|
// only s.reserved (its own lock), so it is safe from any context.
|
||||||
|
func (s *CollectibleUsernameStore) nameReserved(usernameLower string) bool {
|
||||||
|
if s == nil || s.reserved == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
r, _ := s.reserved.IsReserved(context.Background(), usernameLower)
|
r, _ := s.reserved.IsReserved(context.Background(), usernameLower)
|
||||||
|
|
@ -119,7 +121,7 @@ func (s *CollectibleUsernameStore) SetEditableUsername(_ context.Context, peer d
|
||||||
return false, domain.ErrUsernameInvalid
|
return false, domain.ErrUsernameInvalid
|
||||||
}
|
}
|
||||||
key := strings.ToLower(username)
|
key := strings.ToLower(username)
|
||||||
if s.nameReservedLocked(key) {
|
if s.nameReserved(key) {
|
||||||
return false, domain.ErrUsernameOccupied
|
return false, domain.ErrUsernameOccupied
|
||||||
}
|
}
|
||||||
if existing, ok := s.registry[key]; ok {
|
if existing, ok := s.registry[key]; ok {
|
||||||
|
|
@ -334,7 +336,7 @@ func (s *CollectibleUsernameStore) MintCollectibleUsername(_ context.Context, re
|
||||||
if _, ok := s.registry[key]; ok {
|
if _, ok := s.registry[key]; ok {
|
||||||
return domain.CollectibleUsername{}, false, domain.ErrUsernameOccupied
|
return domain.CollectibleUsername{}, false, domain.ErrUsernameOccupied
|
||||||
}
|
}
|
||||||
if s.nameReservedLocked(key) {
|
if s.nameReserved(key) {
|
||||||
return domain.CollectibleUsername{}, false, domain.ErrUsernameOccupied
|
return domain.CollectibleUsername{}, false, domain.ErrUsernameOccupied
|
||||||
}
|
}
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
|
||||||
37
internal/store/memory/reserved_username_test.go
Normal file
37
internal/store/memory/reserved_username_test.go
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package memory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"telesrv/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckUsernameReportsReservedAsTaken(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
reserved := NewReservedUsernameStore()
|
||||||
|
if _, err := reserved.ReserveUsername(ctx, "support", "official", "ops"); err != nil {
|
||||||
|
t.Fatalf("seed reserve: %v", err)
|
||||||
|
}
|
||||||
|
registry := NewCollectibleUsernameStore().WithReservedUsernames(reserved)
|
||||||
|
|
||||||
|
users := NewUserStore()
|
||||||
|
users.AttachUsernameRegistry(registry)
|
||||||
|
u, _ := users.Create(ctx, domain.User{AccessHash: 1, Phone: "15550001000", FirstName: "A"})
|
||||||
|
if ok, err := users.CheckUsername(ctx, u.ID, "support"); err != nil || ok {
|
||||||
|
t.Fatalf("CheckUsername(reserved) = %v, %v; want false, nil", ok, err)
|
||||||
|
}
|
||||||
|
if ok, err := users.CheckUsername(ctx, u.ID, "freename"); err != nil || !ok {
|
||||||
|
t.Fatalf("CheckUsername(free) = %v, %v; want true, nil", ok, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
channels := NewChannelStore()
|
||||||
|
channels.AttachUsernameRegistry(registry)
|
||||||
|
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{CreatorUserID: u.ID, Title: "C", Megagroup: true, Date: 1})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create channel: %v", err)
|
||||||
|
}
|
||||||
|
if ok, err := channels.CheckUsername(ctx, u.ID, created.Channel.ID, "support"); err != nil || ok {
|
||||||
|
t.Fatalf("channel CheckUsername(reserved) = %v, %v; want false, nil", ok, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -158,6 +158,9 @@ func (s *UserStore) CheckUsername(_ context.Context, userID int64, username stri
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
if s.usernameRegistry != nil && s.usernameRegistry.nameReserved(username) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
for id, u := range s.byID {
|
for id, u := range s.byID {
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,11 @@ func usernameReservedTx(ctx context.Context, db sqlcgen.DBTX, usernameLower stri
|
||||||
}
|
}
|
||||||
|
|
||||||
func peerUsernameAvailable(ctx context.Context, db sqlcgen.DBTX, usernameLower, peerType string, peerID int64) (bool, error) {
|
func peerUsernameAvailable(ctx context.Context, db sqlcgen.DBTX, usernameLower, peerType string, peerID int64) (bool, error) {
|
||||||
|
if reserved, err := usernameReservedTx(ctx, db, usernameLower); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if reserved {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
owner, found, err := getPeerUsernameOwner(ctx, db, usernameLower, false)
|
owner, found, err := getPeerUsernameOwner(ctx, db, usernameLower, false)
|
||||||
if err != nil || !found {
|
if err != nil || !found {
|
||||||
return !found, err
|
return !found, err
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue