feat: add NFT usernames and bot verification (#22)

Implements collectible usernames, official verification workflows, and third-party bot verification after maintainer protocol and migration review.

The composite activity/moderation rating remains an admin-only read model; Telegram Stars Rating wire fields stay unset pending a dedicated official-semantics implementation.

Reviewed-Head: 2796345775ea0f908fb7734601e5e1dee4b653b9
Original-Head: fa082b892fd5180c9c9bc53c81c21cf5d250a75b

Co-authored-by: Egor Egorov <business.egor.sg@gmail.com>
This commit is contained in:
Egor Egorov 2026-07-27 20:18:00 +03:00 • committed by GitHub
parent b0fd3976f1
commit fff8de783a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
169 changed files with 55769 additions and 282 deletions

View file

@ -0,0 +1,64 @@
package rpc
import (
"context"
"fmt"
"telesrv/internal/domain"
)
// NotifyPeerUsernamesChanged is the domain-only edge hook invoked after a
// collectible username registry mutation commits. It invalidates the cached
// peer projection and pushes the ordinary non-PTS updateUser/updateChannel
// refresh to online viewers. The shared projection paths preload the registry
// once, so fan-out cannot turn the change into an N+1 query.
func (r *Router) NotifyPeerUsernamesChanged(ctx context.Context, peer domain.Peer) error {
if r == nil {
return nil
}
if peer.ID <= 0 {
return fmt.Errorf("notify peer usernames changed: invalid peer id %d", peer.ID)
}
switch peer.Type {
case domain.PeerTypeUser:
return r.notifyUserUsernamesChanged(ctx, peer.ID)
case domain.PeerTypeChannel:
return r.notifyChannelUsernamesChanged(ctx, peer.ID)
default:
return fmt.Errorf("notify peer usernames changed: unsupported peer type %q for peer %d", peer.Type, peer.ID)
}
}
func (r *Router) notifyUserUsernamesChanged(ctx context.Context, userID int64) error {
r.invalidateRPCProjectionForUser(userID)
if r.deps.Users == nil {
return nil
}
user, found, err := r.verificationUser(ctx, userID)
if err != nil {
return fmt.Errorf("notify peer usernames changed: load user %d: %w", userID, err)
}
if !found || user.ID == 0 {
return fmt.Errorf("notify peer usernames changed: user %d not found", userID)
}
return r.NotifyUserModerationFlagsChanged(ctx, user)
}
func (r *Router) notifyChannelUsernamesChanged(ctx context.Context, channelID int64) error {
r.invalidateRPCProjectionForChannel(channelID)
if r.deps.Channels == nil {
return nil
}
directory, ok := r.deps.Channels.(verificationChannelDirectory)
if !ok {
return fmt.Errorf("notify peer usernames changed: channel service does not expose GetChannelByID")
}
channel, err := directory.GetChannelByID(ctx, channelID)
if err != nil {
return fmt.Errorf("notify peer usernames changed: load channel %d: %w", channelID, err)
}
if channel.ID == 0 {
return fmt.Errorf("notify peer usernames changed: channel %d not found", channelID)
}
return r.NotifyChannelChanged(ctx, channel)
}