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

@ -125,7 +125,7 @@ func (r *Router) onUsersGetUsers(ctx context.Context, ids []tg.InputUserClass) (
}
out = append(out, r.tgUser(u))
}
r.applyStoryMaxIDsToPeerObjects(ctx, currentUserID, out, nil)
r.applyPeerReadModels(ctx, currentUserID, out, nil)
return out, nil
}
@ -159,7 +159,7 @@ func (r *Router) onUsersGetFullUser(ctx context.Context, id tg.InputUserClass) (
return nil, err
}
applyPrivateContactRestrictionToUser(user, contactRestriction)
r.applyStoryMaxIDsToPeerObjects(ctx, currentUserID, []tg.UserClass{user}, nil)
r.applyPeerReadModels(ctx, currentUserID, []tg.UserClass{user}, nil)
loadEpoch := r.userFullProjectionCache.LoadEpoch()
if full, ok := r.userFullProjectionCache.Lookup(currentUserID, u.ID); ok {
if !applyContactNoteToUserFull(u, &full) {
@ -170,6 +170,7 @@ func (r *Router) onUsersGetFullUser(ctx context.Context, id tg.InputUserClass) (
}
r.applyStoriesPinnedAvailableToUserFull(ctx, currentUserID, u.ID, &full)
r.applyNotifySettingsToUserFull(ctx, currentUserID, u.ID, &full)
r.applyBotVerificationToUserFull(ctx, u.ID, &full)
applyPrivateContactRestrictionToUserFull(&full, contactRestriction)
chats := r.applyPersonalChannelToUserFull(ctx, currentUserID, u.PersonalChannelID, &full)
return &tg.UsersUserFull{
@ -191,6 +192,10 @@ func (r *Router) onUsersGetFullUser(ctx context.Context, id tg.InputUserClass) (
}
r.applyStoriesPinnedAvailableToUserFull(ctx, currentUserID, u.ID, &full)
r.applyNotifySettingsToUserFull(ctx, currentUserID, u.ID, &full)
// Deliberately after StoreIfEpoch, like applyPersonalChannelToUserFull: the mark
// is not baked into the per-(viewer,target) cache, so a revoked badge is gone on
// the next response instead of lingering for the cache TTL.
r.applyBotVerificationToUserFull(ctx, u.ID, &full)
applyPrivateContactRestrictionToUserFull(&full, contactRestriction)
chats := r.applyPersonalChannelToUserFull(ctx, currentUserID, u.PersonalChannelID, &full)
return &tg.UsersUserFull{
@ -722,14 +727,16 @@ func (r *Router) fillUserFullPhotos(ctx context.Context, viewerUserID, ownerUser
// TDesktop 的 botInfo.inited 永不置位,每次开聊/输 "/" 都会重拉 getFullUser;
// user_id 必填且必须等于该 bot 的 id,不匹配会被客户端整体静默忽略。
func (r *Router) tgBotInfo(ctx context.Context, u domain.User) tg.BotInfo {
if r.deps.Bots == nil {
return tgBotInfoFromProfile(u.ID, domain.BotProfile{}, false)
info := tgBotInfoFromProfile(u.ID, domain.BotProfile{}, false)
if r.deps.Bots != nil {
if profile, found, err := r.deps.Bots.BotInfo(ctx, u.ID); err == nil && found {
info = tgBotInfoFromProfile(u.ID, profile, true)
}
}
profile, found, err := r.deps.Bots.BotInfo(ctx, u.ID)
if err != nil || !found {
return tgBotInfoFromProfile(u.ID, domain.BotProfile{}, false)
}
return tgBotInfoFromProfile(u.ID, profile, true)
// botInfo#4d8a0299 verifier_settings:flags.9 -- present only for a bot that is
// itself an enabled verifier, and independent of the bot profile above.
r.applyVerifierSettingsToOneBotInfo(ctx, &info, u.ID)
return info
}
type botProfileBatchResolver interface {
@ -755,10 +762,18 @@ func (r *Router) tgBotInfos(ctx context.Context, userIDs []int64) []tg.BotInfo {
}
}
}
// One verifier-settings query for the whole bot list, next to the profile batch
// above: channelFull.bot_info must not turn into an N+1 just to carry
// verifier_settings:flags.9.
verifiers := r.verifierSettingsBatch(ctx, ids)
out := make([]tg.BotInfo, 0, len(userIDs))
for _, id := range userIDs {
profile, found := profiles[id]
out = append(out, tgBotInfoFromProfile(id, profile, found))
info := tgBotInfoFromProfile(id, profile, found)
if settings, ok := verifiers[id]; ok {
applyVerifierSettingsToBotInfo(&info, id, settings)
}
out = append(out, info)
}
return out
}