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

@ -108,6 +108,8 @@ func (r *Router) NotifyUserModerationFlagsChanged(ctx context.Context, u domain.
pushCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
botVerificationIcon := r.peerBotVerificationIcon(pushCtx, domain.Peer{Type: domain.PeerTypeUser, ID: u.ID})
usernames := r.usernameRegistryMap(pushCtx, []domain.Peer{{Type: domain.PeerTypeUser, ID: u.ID}})
seen := make(map[int64]struct{}, len(audience))
for _, viewerUserID := range audience {
if viewerUserID == 0 {
@ -128,9 +130,18 @@ func (r *Router) NotifyUserModerationFlagsChanged(ctx context.Context, u domain.
zap.Error(err))
continue
}
projected := tgUsersForViewer(viewerUserID, users)
// The pushed peer object has to match what users.getUsers would answer, or the
// client refreshes the peer straight back into the stale shape. The third-party
// verification icon (user#b1b8cc83 bot_verification_icon:flags2.14) lives in a
// read model rather than on domain.User, so it is stamped on here -- from the
// single read taken before the loop, since it is the same peer for every
// recipient. Zero leaves flags2.14 unset, which is the pre-feature shape.
applyBotVerificationIconToUsers(projected, u.ID, botVerificationIcon)
applyUsernamesFromRegistry(projected, nil, usernames)
r.pushUserUpdates(pushCtx, viewerUserID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateUser{UserID: u.ID}},
Users: tgUsersForViewer(viewerUserID, users),
Users: projected,
Date: int(r.clock.Now().Unix()),
Seq: 0,
})