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:
parent
b0fd3976f1
commit
fff8de783a
169 changed files with 55769 additions and 282 deletions
117
internal/rpc/verification_notify.go
Normal file
117
internal/rpc/verification_notify.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// verificationUserDirectory is the optional viewer-independent account reader.
|
||||
// users.Service satisfies it; it is preferred over UsersService.ByID because
|
||||
// "does this peer exist" must not depend on a viewer projection.
|
||||
type verificationUserDirectory interface {
|
||||
AdminUser(ctx context.Context, userID int64) (domain.User, bool, error)
|
||||
}
|
||||
|
||||
// verificationChannelDirectory is the optional non-personalized channel base-row
|
||||
// reader. channels.Service satisfies it (see the same assertion in communities.go);
|
||||
// the badge hook has no viewer of its own, so it cannot use GetChannel.
|
||||
type verificationChannelDirectory interface {
|
||||
GetChannelByID(ctx context.Context, channelID int64) (domain.Channel, error)
|
||||
}
|
||||
|
||||
// NotifyPeerVerified is the protocol-edge hook the official verification service
|
||||
// invokes after an approve/reject/revoke decision has already committed. It makes
|
||||
// the new user#b1b8cc83 verified:flags.17 / channel#d49f34c6 verified:flags.7 bit
|
||||
// observable without waiting for a cache TTL:
|
||||
//
|
||||
// - the cached peer projections for the target are dropped, so the next
|
||||
// users.getFullUser / channels.getFullChannel rebuilds from the committed row;
|
||||
// - online clients that already know the peer are pushed the ordinary, non-PTS
|
||||
// refresh update (updateUser / updateChannel) together with the re-projected
|
||||
// peer object, which is what flips the badge in an official client live.
|
||||
//
|
||||
// It deliberately reuses the paths the scam/fake moderation flags already use
|
||||
// rather than inventing a verification-specific update: the badge is one more
|
||||
// boolean on the same peer record, and a second mechanism could only drift.
|
||||
//
|
||||
// Offline sessions are not pushed to and do not need to be: verified is part of
|
||||
// the peer's base read model, whose version is bumped by the users/channels
|
||||
// triggers in 0001_init, so updates.getDifference and any later getFullUser /
|
||||
// getUsers answer already carries the new flag.
|
||||
//
|
||||
// A push failure never invalidates the committed decision, so the caller logs and
|
||||
// swallows the returned error; this method therefore reports problems instead of
|
||||
// panicking, and is safe on a nil receiver.
|
||||
func (r *Router) NotifyPeerVerified(ctx context.Context, peer domain.Peer) error {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
if peer.ID <= 0 {
|
||||
return fmt.Errorf("notify peer verified: invalid peer id %d", peer.ID)
|
||||
}
|
||||
switch peer.Type {
|
||||
case domain.PeerTypeUser:
|
||||
return r.notifyUserVerified(ctx, peer.ID)
|
||||
case domain.PeerTypeChannel:
|
||||
return r.notifyChannelVerified(ctx, peer.ID)
|
||||
default:
|
||||
return fmt.Errorf("notify peer verified: unsupported peer type %q for peer %d", peer.Type, peer.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// notifyUserVerified covers ordinary accounts and bots alike: a verified bot is a
|
||||
// user#b1b8cc83 with verified:flags.17, so it takes the same audience-wide
|
||||
// updateUser fan-out the moderation flags use (owner plus every online account
|
||||
// that already sees the peer).
|
||||
func (r *Router) notifyUserVerified(ctx context.Context, userID int64) error {
|
||||
// Invalidate first and unconditionally: a decided application whose projection
|
||||
// still says "not verified" would keep serving the stale badge state even if the
|
||||
// push below cannot run.
|
||||
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 verified: load user %d: %w", userID, err)
|
||||
}
|
||||
if !found || user.ID == 0 {
|
||||
return fmt.Errorf("notify peer verified: user %d not found", userID)
|
||||
}
|
||||
// NotifyUserModerationFlagsChanged re-projects the peer per recipient, so the
|
||||
// snapshot handed in only carries identity; the pushed tg.User is always built
|
||||
// from a fresh read.
|
||||
return r.NotifyUserModerationFlagsChanged(ctx, user)
|
||||
}
|
||||
|
||||
func (r *Router) verificationUser(ctx context.Context, userID int64) (domain.User, bool, error) {
|
||||
if directory, ok := r.deps.Users.(verificationUserDirectory); ok {
|
||||
return directory.AdminUser(ctx, userID)
|
||||
}
|
||||
return r.deps.Users.ByID(ctx, userID, userID)
|
||||
}
|
||||
|
||||
// notifyChannelVerified reuses the channel state-mutation path, which invalidates
|
||||
// the channel projections (plus a linked monoforum's) and pushes updateChannel
|
||||
// with the refreshed chat object to the channel's members.
|
||||
func (r *Router) notifyChannelVerified(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 verified: channel service does not expose GetChannelByID")
|
||||
}
|
||||
channel, err := directory.GetChannelByID(ctx, channelID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("notify peer verified: load channel %d: %w", channelID, err)
|
||||
}
|
||||
if channel.ID == 0 {
|
||||
return fmt.Errorf("notify peer verified: channel %d not found", channelID)
|
||||
}
|
||||
// Same hook the admin panel uses for any other channel base fact.
|
||||
return r.NotifyChannelChanged(ctx, channel)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue