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
146
internal/rpc/channels_invites_verified_test.go
Normal file
146
internal/rpc/channels_invites_verified_test.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/iamxvbaba/td/bin"
|
||||
"github.com/iamxvbaba/td/clock"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// inviteBadgeChannels answers messages.checkChatInvite from a canned persistent
|
||||
// channel record, so the test observes exactly the flags the projection derives.
|
||||
type inviteBadgeChannels struct {
|
||||
ChannelsService
|
||||
result domain.CheckChannelInviteResult
|
||||
}
|
||||
|
||||
func (s *inviteBadgeChannels) CheckInvite(_ context.Context, _ int64, _ string, _ int) (domain.CheckChannelInviteResult, error) {
|
||||
return s.result, nil
|
||||
}
|
||||
|
||||
func checkChatInvitePreview(t *testing.T, ch domain.Channel) *tg.ChatInvite {
|
||||
t.Helper()
|
||||
r := New(Config{}, Deps{Channels: &inviteBadgeChannels{
|
||||
result: domain.CheckChannelInviteResult{
|
||||
Channel: ch,
|
||||
Invite: domain.ChannelInvite{Hash: "hash", ChannelID: ch.ID},
|
||||
},
|
||||
}}, zap.NewNop(), clock.System)
|
||||
res, err := r.onMessagesCheckChatInvite(WithUserID(context.Background(), 1001), "hash")
|
||||
if err != nil {
|
||||
t.Fatalf("check chat invite: %v", err)
|
||||
}
|
||||
invite, ok := res.(*tg.ChatInvite)
|
||||
if !ok {
|
||||
t.Fatalf("invite = %T %+v", res, res)
|
||||
}
|
||||
// Round-trip through the wire so the assertions read the encoded flags word
|
||||
// rather than only the Go struct fields: a raw field assignment would pass the
|
||||
// struct check and still ship flags.7/8/9 unset.
|
||||
buf := &bin.Buffer{}
|
||||
if err := invite.Encode(buf); err != nil {
|
||||
t.Fatalf("encode chatInvite: %v", err)
|
||||
}
|
||||
decoded := &tg.ChatInvite{}
|
||||
if err := decoded.Decode(buf); err != nil {
|
||||
t.Fatalf("decode chatInvite: %v", err)
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
// TestCheckChatInvitePreviewCarriesLayer228BadgeFlags pins chatInvite#5c9d3702
|
||||
// verified:flags.7 / scam:flags.8 / fake:flags.9 onto the invite preview, so an
|
||||
// official client shows the badge before the user joins rather than after.
|
||||
func TestCheckChatInvitePreviewCarriesLayer228BadgeFlags(t *testing.T) {
|
||||
if tg.ChatInviteTypeID != 0x5c9d3702 {
|
||||
t.Fatalf("chatInvite constructor id = %#x", tg.ChatInviteTypeID)
|
||||
}
|
||||
|
||||
verified := checkChatInvitePreview(t, domain.Channel{
|
||||
ID: 4004, AccessHash: 44, Title: "Official", Username: "official",
|
||||
Broadcast: true, ParticipantsCount: 7, Verified: true,
|
||||
})
|
||||
if !verified.GetVerified() || !verified.Verified {
|
||||
t.Fatalf("verified invite = %+v", verified)
|
||||
}
|
||||
if verified.GetScam() || verified.GetFake() {
|
||||
t.Fatalf("verified invite leaked moderation flags: %+v", verified)
|
||||
}
|
||||
if !verified.Channel || !verified.Broadcast || !verified.Public ||
|
||||
verified.Title != "Official" || verified.ParticipantsCount != 7 {
|
||||
t.Fatalf("verified invite lost unrelated fields: %+v", verified)
|
||||
}
|
||||
|
||||
flagged := checkChatInvitePreview(t, domain.Channel{
|
||||
ID: 4005, AccessHash: 45, Title: "Flagged", Megagroup: true,
|
||||
Scam: true, Fake: true,
|
||||
})
|
||||
if !flagged.GetScam() || !flagged.GetFake() {
|
||||
t.Fatalf("flagged invite = %+v", flagged)
|
||||
}
|
||||
if flagged.GetVerified() {
|
||||
t.Fatalf("flagged invite claims verification: %+v", flagged)
|
||||
}
|
||||
if !flagged.Megagroup || flagged.Public {
|
||||
t.Fatalf("flagged invite lost unrelated fields: %+v", flagged)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckChatInvitePreviewLeavesUnflaggedChannelClean is the negative half: an
|
||||
// ordinary channel must not carry flags.7/8/9 at all, so the encoded preview stays
|
||||
// byte-identical to what an official server sends for an unflagged peer.
|
||||
func TestCheckChatInvitePreviewLeavesUnflaggedChannelClean(t *testing.T) {
|
||||
plain := checkChatInvitePreview(t, domain.Channel{
|
||||
ID: 4006, AccessHash: 46, Title: "Plain", Broadcast: true,
|
||||
})
|
||||
if plain.GetVerified() || plain.Verified {
|
||||
t.Fatalf("unverified channel exposed verified: %+v", plain)
|
||||
}
|
||||
if plain.GetScam() || plain.GetFake() || plain.Scam || plain.Fake {
|
||||
t.Fatalf("unflagged channel exposed moderation flags: %+v", plain)
|
||||
}
|
||||
if plain.Flags.Has(7) || plain.Flags.Has(8) || plain.Flags.Has(9) {
|
||||
t.Fatalf("unflagged channel set badge bits in flags word: %d", plain.Flags)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckChatInviteAlreadyKeepsChannelBadge guards the other branch: a member
|
||||
// gets chatInviteAlready#5a686d7c, whose Chat already carries
|
||||
// channel#d49f34c6 verified:flags.7 through tgChannelChat, so the flags must not be
|
||||
// applied twice or lost.
|
||||
func TestCheckChatInviteAlreadyKeepsChannelBadge(t *testing.T) {
|
||||
const channelID = int64(4007)
|
||||
r := New(Config{}, Deps{Channels: &inviteBadgeChannels{
|
||||
result: domain.CheckChannelInviteResult{
|
||||
Channel: domain.Channel{
|
||||
ID: channelID, AccessHash: 47, Title: "Official", Username: "official",
|
||||
Broadcast: true, Verified: true,
|
||||
},
|
||||
Invite: domain.ChannelInvite{Hash: "hash", ChannelID: channelID},
|
||||
Already: true,
|
||||
Self: domain.ChannelMember{
|
||||
ChannelID: channelID, UserID: 1001, Status: domain.ChannelMemberActive,
|
||||
},
|
||||
},
|
||||
}}, zap.NewNop(), clock.System)
|
||||
res, err := r.onMessagesCheckChatInvite(WithUserID(context.Background(), 1001), "hash")
|
||||
if err != nil {
|
||||
t.Fatalf("check chat invite: %v", err)
|
||||
}
|
||||
already, ok := res.(*tg.ChatInviteAlready)
|
||||
if !ok {
|
||||
t.Fatalf("invite = %T %+v", res, res)
|
||||
}
|
||||
channel, ok := already.Chat.(*tg.Channel)
|
||||
if !ok || channel.ID != channelID {
|
||||
t.Fatalf("chat = %T %+v", already.Chat, already.Chat)
|
||||
}
|
||||
if !channel.Verified || channel.Scam || channel.Fake {
|
||||
t.Fatalf("already-member channel badge = %+v", channel)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue