changed verifier bot to marksbot
This commit is contained in:
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
1
cmd/telesrv-admin/web/dist/assets/index-EkAGiEK9.css
vendored
Normal file
1
cmd/telesrv-admin/web/dist/assets/index-EkAGiEK9.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
cmd/telesrv-admin/web/dist/index.html
vendored
4
cmd/telesrv-admin/web/dist/index.html
vendored
|
|
@ -23,8 +23,8 @@
|
|||
})();
|
||||
</script>
|
||||
|
||||
<script type="module" crossorigin src="/assets/index-65rEwtSD.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CKoIcj6p.css">
|
||||
<script type="module" crossorigin src="/assets/index-D0qBFBU-.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-EkAGiEK9.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
130
cmd/telesrv-admin/web/src/components/EmojiPicker.tsx
Normal file
130
cmd/telesrv-admin/web/src/components/EmojiPicker.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { api, APIError, errorMessage } from "../api";
|
||||
import { ActionButton } from "../components/ActionButton";
|
||||
import { EmojiPicker } from "../components/EmojiPicker";
|
||||
import { BotPicker } from "../components/EntityPicker";
|
||||
import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel, SectionHead } from "../components/ui";
|
||||
import { displayUsername, formatDate } from "../lib/format";
|
||||
|
|
@ -623,17 +624,9 @@ function IconsBlock({
|
|||
<>
|
||||
{canManage && (
|
||||
<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">
|
||||
<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">
|
||||
<span>{"Name"}</span>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -11,9 +11,8 @@ import { StickerSetPreviewModal } from "./StickerSetPreviewModal";
|
|||
type StickerPageSize = 10 | 20 | 50 | 100 | "all";
|
||||
|
||||
// Shared list/manage view for one non-system sticker-set kind ("stickers" or
|
||||
// "emoji") — system packs (dice, animated emoji, premium/TON gifts, etc.) are
|
||||
// filtered out server-side and never reach this page; they aren't meant to be
|
||||
// hand-edited.
|
||||
// "emoji") — system packs (dice, animated emoji, gifts) aren't shown here,
|
||||
// they're not hand-edited.
|
||||
export function StickerSetsPage({ kind }: { kind: "stickers" | "emoji" }) {
|
||||
const [sets, setSets] = useState<StickerSetRow[]>([]);
|
||||
const [query, setQuery] = useState("");
|
||||
|
|
|
|||
|
|
@ -317,6 +317,26 @@
|
|||
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 {
|
||||
color: var(--danger);
|
||||
background: var(--danger-tint);
|
||||
|
|
|
|||
|
|
@ -1223,9 +1223,14 @@ func run(logger *zap.Logger) error {
|
|||
botverificationapp.WithMaxPerVerifier(cfg.BotVerificationMaxPerVerifier),
|
||||
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)
|
||||
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")))
|
||||
router := rpc.New(rpc.Config{
|
||||
DC: cfg.DC,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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';
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
UPDATE public.users
|
||||
SET verified = false,
|
||||
updated_at = now()
|
||||
WHERE id = 1250000013;
|
||||
12
deploy/migrations/20260714003127_marks_bot_verified.up.sql
Normal file
12
deploy/migrations/20260714003127_marks_bot_verified.up.sql
Normal 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;
|
||||
55
internal/app/botverification/seed.go
Normal file
55
internal/app/botverification/seed.go
Normal 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
|
||||
}
|
||||
|
|
@ -48,18 +48,29 @@ const (
|
|||
// migration 0153; the two must never drift.
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
// 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
|
||||
// VerifierBotAccessHash is fixed and double-written with the seed row in
|
||||
// migration 0156; the two must never drift.
|
||||
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
|
||||
|
|
@ -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
|
||||
// mechanism, and a third-party verifier wearing it would blur exactly the
|
||||
// distinction this bot has to explain to every applicant. What makes the account a
|
||||
// verifier is the operator-granted BotVerifierSettings row, not this seed.
|
||||
// Verified is true: this deployment carries the platform checkmark on its own
|
||||
// service bots (see e.g. VerifyBotUser, BotFatherUser), and @marksbot is one of
|
||||
// them -- a legitimate first-party account, just one that happens to also grant
|
||||
// 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 {
|
||||
return User{
|
||||
ID: VerifierBotUserID,
|
||||
AccessHash: VerifierBotAccessHash,
|
||||
FirstName: "Verifier Bot",
|
||||
Username: "verifierbot",
|
||||
Verified: false,
|
||||
FirstName: "Marks Bot",
|
||||
Username: "marksbot",
|
||||
Verified: true,
|
||||
Bot: true,
|
||||
BotInfoVersion: 1,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("migrate star gift lifecycle schema: %v", err)
|
||||
}
|
||||
if status.Dirty || status.Empty || status.Version != 20260714003125 {
|
||||
t.Fatalf("migration status = %+v, want clean version 20260714003125", status)
|
||||
if status.Dirty || status.Empty || status.Version != 20260714003127 {
|
||||
t.Fatalf("migration status = %+v, want clean version 20260714003127", status)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue