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

@ -64,6 +64,12 @@ func (r *Router) registerAccount(d *tlprofile.Dispatcher) {
return r.onAccountUpdateUsername(ctx, layerRequest.
Username)
})
registerRPC[*tg.AccountReorderUsernamesRequest](d, tlprofile.SemanticMethodAccountReorderUsernames, func(ctx context.Context, layerRequest *tg.AccountReorderUsernamesRequest) (any, error) {
return r.onAccountReorderUsernames(ctx, layerRequest)
})
registerRPC[*tg.AccountToggleUsernameRequest](d, tlprofile.SemanticMethodAccountToggleUsername, func(ctx context.Context, layerRequest *tg.AccountToggleUsernameRequest) (any, error) {
return r.onAccountToggleUsername(ctx, layerRequest)
})
registerRPC[*tg.AccountUpdateBirthdayRequest](d, tlprofile.SemanticMethodAccountUpdateBirthday, func(ctx context.Context, layerRequest *tg.AccountUpdateBirthdayRequest) (any, error) {
return r.onAccountUpdateBirthday(ctx, layerRequest)
})
@ -1555,6 +1561,54 @@ func (r *Router) onAccountUpdateUsername(ctx context.Context, username string) (
return r.tgSelfUser(u), nil
}
// onAccountReorderUsernames rewrites the caller's active username order
// (account.reorderUsernames). The editable slot is included when active, just as
// it is in the vector sent by official clients.
//
// Without a username registry the account owns a single editable username, which
// has no order to change: USERNAME_NOT_MODIFIED is the accurate answer and keeps
// clients from believing a reorder took effect.
func (r *Router) onAccountReorderUsernames(ctx context.Context, req *tg.AccountReorderUsernamesRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if req == nil {
return false, usernameInvalidErr()
}
if len(req.Order) > domain.MaxPeerCollectibleUsernames+1 {
return false, limitInvalidErr()
}
if r.deps.Usernames == nil {
return false, usernameNotModifiedErr()
}
if err := r.reorderRegistryUsernames(ctx, domain.Peer{Type: domain.PeerTypeUser, ID: userID}, req.Order); err != nil {
return false, err
}
return true, nil
}
// onAccountToggleUsername activates/deactivates one of the caller's own
// collectible usernames (account.toggleUsername). Deactivating the editable slot
// through this method is rejected by the domain rules with USERNAME_INVALID:
// clearing the editable username is account.updateUsername's job.
func (r *Router) onAccountToggleUsername(ctx context.Context, req *tg.AccountToggleUsernameRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if req == nil {
return false, usernameInvalidErr()
}
if r.deps.Usernames == nil {
return false, usernameNotModifiedErr()
}
if err := r.toggleRegistryUsername(ctx, domain.Peer{Type: domain.PeerTypeUser, ID: userID}, req.Username, req.Active); err != nil {
return false, err
}
return true, nil
}
// onAccountUpdateBirthday 持久化资料页生日(account.updateBirthday)。birthday 缺省即清除;
// 月/日/年非法返回 BIRTHDAY_INVALID。生日落在 userFull(按隐私 PrivacyKeyBirthday 对外裁剪)。
// 写入后推 updateUser 信号给本人其它在线 session,促使已加载 full profile 的客户端重拉。
@ -1869,14 +1923,24 @@ func (r *Router) pushUsernameUpdate(ctx context.Context, u domain.User) {
if u.ID == 0 {
return
}
// updateUserName carries the vector clients persist, so it has to be the full
// registry list when one exists. One peer, one registry read; the overlay
// degrades to tgUsernames(u.Username) whenever the registry is unavailable.
self := r.tgSelfUser(u)
users := []tg.UserClass{self}
r.applyUsernamesToPeerObjects(ctx, users, nil)
usernames := tgUsernames(u.Username)
if vector, ok := self.GetUsernames(); ok && len(vector) > 0 {
usernames = vector
}
r.pushUserUpdates(ctx, u.ID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateUserName{
UserID: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
Usernames: tgUsernames(u.Username),
Usernames: usernames,
}},
Users: []tg.UserClass{r.tgSelfUser(u)},
Users: users,
Date: int(r.clock.Now().Unix()),
})
}