changed verifier bot to marksbot

This commit is contained in:
onysd 2026-08-04 03:22:46 +03:00
parent 6ade34970c
commit fa5cfaf14d
16 changed files with 302 additions and 30 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -23,8 +23,8 @@
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-65rEwtSD.js"></script> <script type="module" crossorigin src="/assets/index-D0qBFBU-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CKoIcj6p.css"> <link rel="stylesheet" crossorigin href="/assets/index-EkAGiEK9.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -0,0 +1,130 @@
import { Check, Loader2, Search, X } from "lucide-react";
import { useEffect, useState } from "react";
import { api, errorMessage } from "../api";
import type { EmojiRow } from "../types";
import { StaticLottie } from "./StaticLottie";
function isAnimated(mime: string): boolean {
const m = mime.toLowerCase();
return m.includes("tgsticker") || m.includes("lottie") || m.includes("json");
}
function EmojiThumb({ row }: { row: EmojiRow }) {
const [failed, setFailed] = useState(!isAnimated(row.MimeType));
useEffect(() => {
setFailed(!isAnimated(row.MimeType));
}, [row.DocumentID, row.MimeType]);
if (failed) {
return <div className="emoji-picker-glyph">{row.Alt || "🙂"}</div>;
}
return (
<StaticLottie
className="emoji-picker-anim"
cacheKey={row.DocumentID}
loader={() => api.emojiAnimation(row.DocumentID)}
onError={() => setFailed(true)}
/>
);
}
// EmojiPicker searches every custom-emoji document already on this
// deployment -- unlike the Emoji admin page's per-pack browsing, this is a
// flat document search with no system/non-system distinction, so a bundled
// icon from a default pack (e.g. Topics' ✅) is just as findable as a
// hand-uploaded one. Lets the caller select a document id by clicking it
// instead of typing one from memory; used wherever a raw document id field
// otherwise has nothing else in the panel to pick from (e.g. a
// bot-verification icon).
export function EmojiPicker({
label,
value,
onChange
}: {
label: string;
value: string;
onChange: (documentID: string) => void;
}) {
const [query, setQuery] = useState("");
const [rows, setRows] = useState<EmojiRow[]>([]);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const selected = rows.find((row) => row.DocumentID === value) ?? null;
async function search() {
setBusy(true);
setError("");
const params = new URLSearchParams({ limit: "24" });
if (query.trim()) params.set("q", query.trim());
try {
const result = await api.emoji(params);
setRows(result.rows ?? []);
} catch (err) {
setError(errorMessage(err));
} finally {
setBusy(false);
}
}
useEffect(() => {
void search();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="entity-picker">
<div className="picker-head">
<span>{label}</span>
{value ? (
<button className="link-button" type="button" onClick={() => onChange("")}>
<X size={13} /> {"Clear"}
</button>
) : null}
</div>
{value ? (
<div className="selected-entity">
<Check size={15} />
<div>
<strong>{selected?.Alt || "—"}</strong>
<span className="mono">{value}</span>
</div>
<span>{selected?.SetTitle || "-"}</span>
</div>
) : null}
<div className="picker-search">
<Search size={15} />
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
void search();
}
}}
placeholder={"Search document ID or emoji"}
/>
<button className="btn compact-btn" type="button" onClick={search} disabled={busy}>
{busy ? <Loader2 size={14} className="spin" /> : "Search"}
</button>
</div>
{error && <div className="picker-error">{error}</div>}
<div className="picker-results emoji-picker-results">
{rows.map((row) => (
<button
key={row.DocumentID}
className={`picker-row emoji-picker-row ${value === row.DocumentID ? "selected" : ""}`}
type="button"
onClick={() => onChange(row.DocumentID)}
>
<EmojiThumb row={row} />
<span className="mono">{row.DocumentID}</span>
<span>{row.SetTitle || "—"}</span>
</button>
))}
{rows.length === 0 && !busy ? <div className="picker-empty">{"No results"}</div> : null}
</div>
</div>
);
}

View file

@ -18,6 +18,7 @@ import {
import { useEffect, useState, type ReactNode } from "react"; import { useEffect, useState, type ReactNode } from "react";
import { api, APIError, errorMessage } from "../api"; import { api, APIError, errorMessage } from "../api";
import { ActionButton } from "../components/ActionButton"; import { ActionButton } from "../components/ActionButton";
import { EmojiPicker } from "../components/EmojiPicker";
import { BotPicker } from "../components/EntityPicker"; import { BotPicker } from "../components/EntityPicker";
import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel, SectionHead } from "../components/ui"; import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel, SectionHead } from "../components/ui";
import { displayUsername, formatDate } from "../lib/format"; import { displayUsername, formatDate } from "../lib/format";
@ -623,17 +624,9 @@ function IconsBlock({
<> <>
{canManage && ( {canManage && (
<section className="section-block"> <section className="section-block">
<SectionHead title={"Add or rename an icon"} text={"The document id has to name a real custom emoji document on this deployment; the Emoji section lists them with their ids. Adding an id that already exists renames it instead of duplicating it."} /> <SectionHead title={"Add or rename an icon"} text={"Search and pick any custom-emoji document already on this deployment (including bundled/system ones). Adding an id that already exists renames it instead of duplicating it."} />
<EmojiPicker label={"Document"} value={documentID} onChange={setDocumentID} />
<div className="bot-create-fields"> <div className="bot-create-fields">
<label className="duration-field">
<span>{"Document ID"}</span>
<input
value={documentID}
onChange={(event) => setDocumentID(event.target.value)}
inputMode="numeric"
placeholder="5361371319611781774"
/>
</label>
<label className="duration-field"> <label className="duration-field">
<span>{"Name"}</span> <span>{"Name"}</span>
<input <input

View file

@ -11,9 +11,8 @@ import { StickerSetPreviewModal } from "./StickerSetPreviewModal";
type StickerPageSize = 10 | 20 | 50 | 100 | "all"; type StickerPageSize = 10 | 20 | 50 | 100 | "all";
// Shared list/manage view for one non-system sticker-set kind ("stickers" or // Shared list/manage view for one non-system sticker-set kind ("stickers" or
// "emoji") — system packs (dice, animated emoji, premium/TON gifts, etc.) are // "emoji") — system packs (dice, animated emoji, gifts) aren't shown here,
// filtered out server-side and never reach this page; they aren't meant to be // they're not hand-edited.
// hand-edited.
export function StickerSetsPage({ kind }: { kind: "stickers" | "emoji" }) { export function StickerSetsPage({ kind }: { kind: "stickers" | "emoji" }) {
const [sets, setSets] = useState<StickerSetRow[]>([]); const [sets, setSets] = useState<StickerSetRow[]>([]);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");

View file

@ -317,6 +317,26 @@
text-align: center; text-align: center;
} }
.emoji-picker-row {
grid-template-columns: 36px minmax(140px, 1fr) minmax(100px, 1fr);
}
.emoji-picker-glyph {
font-size: 22px;
line-height: 1;
text-align: center;
}
.emoji-picker-anim {
width: 28px;
height: 28px;
}
.emoji-picker-anim canvas {
width: 100% !important;
height: 100% !important;
}
.picker-error { .picker-error {
color: var(--danger); color: var(--danger);
background: var(--danger-tint); background: var(--danger-tint);

View file

@ -1223,9 +1223,14 @@ func run(logger *zap.Logger) error {
botverificationapp.WithMaxPerVerifier(cfg.BotVerificationMaxPerVerifier), botverificationapp.WithMaxPerVerifier(cfg.BotVerificationMaxPerVerifier),
botverificationapp.WithLogger(logger.Named("app").Named("botverification")), botverificationapp.WithLogger(logger.Named("app").Named("botverification")),
) )
// @verifierbot files applications with the operator and reports decisions back. // @marksbot files applications with the operator and reports decisions back.
botsService.SetCustomVerification(botVerificationService) botsService.SetCustomVerification(botVerificationService)
botVerificationService.SetApplicantNotifier(botsService) botVerificationService.SetApplicantNotifier(botsService)
if granted, err := botVerificationService.SeedDefaultVerifier(ctx); err != nil {
return fmt.Errorf("seed default verifier: %w", err)
} else if granted {
logger.Info("default verifier seed complete", zap.Int64("bot_id", domain.VerifierBotUserID))
}
updatesService := updates.NewService(updateStateStore, updateEventStore, updates.WithLogger(logger.Named("app").Named("updates"))) updatesService := updates.NewService(updateStateStore, updateEventStore, updates.WithLogger(logger.Named("app").Named("updates")))
router := rpc.New(rpc.Config{ router := rpc.New(rpc.Config{
DC: cfg.DC, DC: cfg.DC,

View file

@ -0,0 +1,11 @@
UPDATE public.peer_usernames
SET username_lower = 'verifierbot',
username = 'verifierbot',
updated_at = now()
WHERE peer_type = 'user' AND peer_id = 1250000013 AND username_lower = 'marksbot';
UPDATE public.users
SET first_name = 'Verifier Bot',
username = 'verifierbot',
updated_at = now()
WHERE id = 1250000013;

View file

@ -0,0 +1,27 @@
-- Renames the built-in third-party-verification bot from @verifierbot
-- ("Verifier Bot") to @marksbot ("Marks Bot").
--
-- The old name was too easy to confuse at a glance with @verifybot ("Verify
-- Bot"), the unrelated official-checkmark front door (0153/0112) -- one grants
-- the platform badge, the other grants a third-party icon+description mark,
-- and the two must never read each other's state (see internal/domain/system.go
-- and 0156/20260714003115). This migration only touches the identity (name,
-- handle); the account id, access_hash and its bot_verifier_settings grant (if
-- any operator already made one) are untouched.
--
-- 20260714003115 already applied on any existing deployment created the
-- account under the old name via INSERT ... ON CONFLICT DO UPDATE -- editing
-- that historical file in place would not reach a database where it already
-- ran, hence a separate migration here instead.
UPDATE public.users
SET first_name = 'Marks Bot',
username = 'marksbot',
updated_at = now()
WHERE id = 1250000013;
UPDATE public.peer_usernames
SET username_lower = 'marksbot',
username = 'marksbot',
updated_at = now()
WHERE peer_type = 'user' AND peer_id = 1250000013 AND username_lower = 'verifierbot';

View file

@ -0,0 +1,4 @@
UPDATE public.users
SET verified = false,
updated_at = now()
WHERE id = 1250000013;

View file

@ -0,0 +1,12 @@
-- @marksbot (formerly @verifierbot) now carries the platform checkmark itself
-- (domain.VerifierBotUser().Verified = true). This is a deliberate operator
-- choice on top of the original upstream design (which kept it unverified to
-- avoid implying the third-party mark it grants is somehow platform-endorsed):
-- the checkmark here only asserts "this account is a legitimate first-party
-- service bot", not "this bot's grants are official" -- the two mechanisms
-- remain fully independent regardless of this flag.
UPDATE public.users
SET verified = true,
updated_at = now()
WHERE id = 1250000013;

View file

@ -0,0 +1,55 @@
package botverification
import (
"context"
"errors"
"fmt"
"telesrv/internal/branding"
"telesrv/internal/domain"
)
// SeedDefaultVerifier idempotently grants @marksbot (domain.VerifierBotUserID)
// verifier status on first boot, using a custom-emoji icon the ordinary
// sticker-seed import already writes (domain.VerifierBotDefaultIconDocumentID)
// -- so the reference third-party verifier has something to demonstrate right
// away instead of an empty icon catalogue and a bot that can only explain
// itself.
//
// Runs at most once: if a bot_verifier_settings row for @marksbot already
// exists -- granted by this seed on a previous boot, or hand-configured by an
// operator who pointed it at a different icon/company -- it is left
// completely untouched. This must never overwrite an operator's own decision
// about their own verifier.
// Returns true if it actually granted verifier status.
func (s *Service) SeedDefaultVerifier(ctx context.Context) (bool, error) {
if s == nil || !s.enabled {
return false, nil
}
if _, err := s.VerifierSettings(ctx, domain.VerifierBotUserID); err == nil {
return false, nil
} else if !errors.Is(err, domain.ErrVerifierNotFound) {
return false, err
}
icon, err := s.UpsertIcon(ctx, domain.VerificationIcon{
DocumentID: domain.VerifierBotDefaultIconDocumentID,
Name: "Default (bundled)",
Active: true,
})
if err != nil {
return false, fmt.Errorf("seed default verifier icon: %w", err)
}
if _, err := s.GrantVerifier(ctx, domain.BotVerifierSettings{
BotID: domain.VerifierBotUserID,
IconDocumentID: icon.DocumentID,
CompanyName: branding.ProductName,
DefaultDescription: "Bundled reference verifier -- auto-granted on first boot.",
CanModifyCustomDescription: false,
Enabled: true,
GrantedBy: "startup-seed",
GrantReason: "Reference verifier auto-granted on first boot so third-party verification has something to demonstrate out of the box.",
}); err != nil {
return false, fmt.Errorf("seed default verifier grant: %w", err)
}
return true, nil
}

View file

@ -48,18 +48,29 @@ const (
// migration 0153; the two must never drift. // migration 0153; the two must never drift.
VerifyBotAccessHash int64 = 7802113947355620887 VerifyBotAccessHash int64 = 7802113947355620887
// VerifierBotUserID is the built-in @verifierbot: the first THIRD-PARTY // VerifierBotUserID is the built-in @marksbot: the first THIRD-PARTY
// verifier of a deployment (core.telegram.org/api/bots/verification). It // verifier of a deployment (core.telegram.org/api/bots/verification). It
// collects applications for its own icon+description mark and reports the // collects applications for its own icon+description mark and reports the
// operator's decision back to the applicant. The id is reserved and stable, so // operator's decision back to the applicant. The id is reserved and stable, so
// a restart never re-creates the account under a different identity. // a restart never re-creates the account under a different identity.
// //
// It is not a second route to the platform checkmark: that badge is granted by // It is not a second route to the platform checkmark: that badge is granted by
// the operator alone and collected by VerifyBotUserID above. // the operator alone and collected by VerifyBotUserID above. Named "Marks Bot"
// rather than anything containing "Verif*" specifically so it can never be
// misread as a second copy of @verifybot -- the two front doors must stay
// visually distinct at a glance, not just distinct in the underlying mechanism.
VerifierBotUserID int64 = 1250000013 VerifierBotUserID int64 = 1250000013
// VerifierBotAccessHash is fixed and double-written with the seed row in // VerifierBotAccessHash is fixed and double-written with the seed row in
// migration 0156; the two must never drift. // migration 0156; the two must never drift.
VerifierBotAccessHash int64 = 6913402578811563729 VerifierBotAccessHash int64 = 6913402578811563729
// VerifierBotDefaultIconDocumentID is the custom-emoji document (a ✅ from the
// default "Topics" emoji set, data/sticker-seed/telegram_emoji_export) reused
// as @marksbot's out-of-the-box icon, so the reference verifier has something
// to grant immediately after first boot instead of an empty catalogue. It is
// bundled media that the ordinary sticker-seed import already writes on every
// startup -- not a document minted specifically for this feature -- so it
// resolves the same way any other seeded custom emoji does.
VerifierBotDefaultIconDocumentID int64 = 5237699328843200968
) )
// officialSystemUserPhotoDCID/Stripped 由 files.Service.SeedOfficialSystemAvatar // officialSystemUserPhotoDCID/Stripped 由 files.Service.SeedOfficialSystemAvatar
@ -209,19 +220,24 @@ func VerifyBotUser() User {
} }
} }
// VerifierBotUser returns the built-in @verifierbot account. // VerifierBotUser returns the built-in @marksbot account.
// //
// Verified is false on purpose: the official checkmark is the platform's own // Verified is true: this deployment carries the platform checkmark on its own
// mechanism, and a third-party verifier wearing it would blur exactly the // service bots (see e.g. VerifyBotUser, BotFatherUser), and @marksbot is one of
// distinction this bot has to explain to every applicant. What makes the account a // them -- a legitimate first-party account, just one that happens to also grant
// verifier is the operator-granted BotVerifierSettings row, not this seed. // a *different*, third-party mark to other peers. The checkmark here says
// "this account is who it claims to be", not "this account's grants are
// official"; that distinction is what @marksbot's own messages explain to every
// applicant, and it does not depend on this bot's own badge being off. What
// makes the account a verifier at all is the operator-granted
// BotVerifierSettings row, not this seed -- the two remain fully independent.
func VerifierBotUser() User { func VerifierBotUser() User {
return User{ return User{
ID: VerifierBotUserID, ID: VerifierBotUserID,
AccessHash: VerifierBotAccessHash, AccessHash: VerifierBotAccessHash,
FirstName: "Verifier Bot", FirstName: "Marks Bot",
Username: "verifierbot", Username: "marksbot",
Verified: false, Verified: true,
Bot: true, Bot: true,
BotInfoVersion: 1, BotInfoVersion: 1,
} }

View file

@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("migrate star gift lifecycle schema: %v", err) t.Fatalf("migrate star gift lifecycle schema: %v", err)
} }
if status.Dirty || status.Empty || status.Version != 20260714003125 { if status.Dirty || status.Empty || status.Version != 20260714003127 {
t.Fatalf("migration status = %+v, want clean version 20260714003125", status) t.Fatalf("migration status = %+v, want clean version 20260714003127", status)
} }
} }