diff --git a/.containerignore b/.containerignore new file mode 100644 index 00000000..64feddb3 --- /dev/null +++ b/.containerignore @@ -0,0 +1,5 @@ +.git +bin +*.pem +.env +.env.* diff --git a/Containerfile b/Containerfile new file mode 100644 index 00000000..149dfe07 --- /dev/null +++ b/Containerfile @@ -0,0 +1,32 @@ +FROM docker.io/library/golang:1.25 AS build +# Build metadata for telesrv's startup log (git_commit/git_branch/... in +# cmd/telesrv/buildinfo.go). .containerignore excludes .git, so go build's +# automatic VCS stamping sees no repo; pass these in explicitly, e.g.: +# podman build \ +# --build-arg GIT_COMMIT="$(git rev-parse HEAD)" \ +# --build-arg GIT_BRANCH="$(git rev-parse --abbrev-ref HEAD)" \ +# --build-arg GIT_TREE_STATE="$(git diff --quiet && echo clean || echo dirty)" \ +# --build-arg BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ +# -t owpengram-server -f Containerfile . +ARG GIT_COMMIT=unknown +ARG GIT_BRANCH=unknown +ARG GIT_TREE_STATE=unknown +ARG BUILD_TIME=unknown +WORKDIR /src +COPY . . +RUN CGO_ENABLED=0 go build -trimpath \ + -ldflags "-X main.gitCommit=${GIT_COMMIT} -X main.gitBranch=${GIT_BRANCH} -X main.gitTreeState=${GIT_TREE_STATE} -X main.buildTime=${BUILD_TIME}" \ + -o /out/gramsrv ./cmd/telesrv +RUN CGO_ENABLED=0 go build -trimpath -o /out/telesrv-admin ./cmd/telesrv-admin +RUN CGO_ENABLED=0 go build -trimpath -o /out/createuser ./cmd/createuser + +FROM docker.io/library/alpine:3.20 +RUN apk add --no-cache ca-certificates tzdata ffmpeg +WORKDIR /app +COPY --from=build /out/gramsrv /app/gramsrv +COPY --from=build /out/telesrv-admin /app/telesrv-admin +COPY --from=build /out/createuser /app/createuser +COPY --from=build /src/data/langpack /app/data/langpack +COPY --from=build /src/data/sticker-seed /app/data/sticker-seed +EXPOSE 2398 2600 +ENTRYPOINT ["/app/gramsrv"] diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..85bcb314 --- /dev/null +++ b/build.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Build the owpengram-server container image (stamping the current git state into +# the binary - .containerignore excludes .git, so go build can't see the repo and +# the values are passed in here), then restart the owpengram-server and +# owpengram-admin systemd services so they pick up the freshly built image. +# Container creation/lifecycle beyond the pod itself is owned by those systemd +# units, not this script. +# +# Usage: ./build.sh [extra podman build args...] +# IMAGE=my/tag ./build.sh override the image tag (default: owpengram-server) +# POD=name ./build.sh override the pod name (default: owpengram) +# NO_DEPLOY=1 ./build.sh build the image only, don't restart the services +set -euo pipefail +cd "$(dirname "$0")" + +IMAGE="${IMAGE:-owpengram-server}" +POD="${POD:-owpengram}" + +podman build \ + --build-arg GIT_COMMIT="$(git rev-parse HEAD)" \ + --build-arg GIT_BRANCH="$(git rev-parse --abbrev-ref HEAD)" \ + --build-arg GIT_TREE_STATE="$(git diff --quiet && echo clean || echo dirty)" \ + --build-arg BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + -t "$IMAGE" \ + -f Containerfile \ + "$@" \ + . + +if [ "${NO_DEPLOY:-0}" = "1" ]; then + echo "built $IMAGE (NO_DEPLOY=1, services unchanged)" + exit 0 +fi + +if ! podman pod exists "$POD"; then + echo "pod '$POD' does not exist - creating it" + podman pod create --name "$POD" \ + -p 2398:2398 \ + -p 127.0.0.1:2600:2600 \ + -p 2400:2400 \ + -p 2500:2500 \ + -p 12399:12399/udp \ + -p 12400:12400/udp \ + -p 12500-12999:12500-12999/udp +fi + +systemctl restart owpengram-server.service owpengram-admin.service +systemctl --no-pager status owpengram-server.service owpengram-admin.service diff --git a/cmd/createuser/main.go b/cmd/createuser/main.go new file mode 100644 index 00000000..d3557418 --- /dev/null +++ b/cmd/createuser/main.go @@ -0,0 +1,181 @@ +// Command createuser inserts a users row with an operator-chosen id, bypassing +// the normal users_id_seq auto-assignment. This works because users.id is +// GENERATED BY DEFAULT AS IDENTITY (not GENERATED ALWAYS) -- an explicit id in +// the INSERT is honored, the same mechanism ensureOfficialSystemUserWithDB +// (internal/store/postgres/message_send.go) already relies on to seed the +// built-in system accounts (ChatBot, BotFather, ...) at their fixed ids. +// +// Normal signup (auth.signUp) never lets a caller pick an id, so this exists +// purely for local/dev tooling -- reserving a specific low id (below +// OfficialSystemUserID=777000, say) for a test account. +// +// Usage: +// +// createuser -id 1000 [-first-name Test] [-last-name User] [-username testuser] -phone "15550001234" +// createuser -id 1000 [-first-name Test] [-last-name User] [-username testuser] -email "test@example.com" +// +// -phone and -email are mutually exclusive: an email-signup account never +// stores the address in users.phone directly (see internal/domain/emailphone.go) +// -- it gets a synthetic "888"-prefixed display phone instead (the same one +// assignEmailSignupDisplayPhone hands a real email-signup account), with the +// real address recorded separately in signup_email. +// +// Reads TELESRV_POSTGRES_DSN the same way the server does (internal/config). +package main + +import ( + "context" + "crypto/rand" + "encoding/binary" + "errors" + "flag" + "fmt" + "os" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" + + "telesrv/internal/config" + "telesrv/internal/domain" +) + +// maxEmailSignupPhoneAttempts bounds the display-phone collision-retry loop, +// mirroring internal/app/auth/service.go's own constant of the same name. +const maxEmailSignupPhoneAttempts = 20 + +func randomInt64() (int64, error) { + var b [8]byte + if _, err := rand.Read(b[:]); err != nil { + return 0, fmt.Errorf("rand: %w", err) + } + return int64(binary.LittleEndian.Uint64(b[:])), nil +} + +func main() { + id := flag.Int64("id", 0, "user id to create (required)") + firstName := flag.String("first-name", "Test", "first name") + lastName := flag.String("last-name", "", "last name") + username := flag.String("username", "", "username, without @ (optional)") + phone := flag.String("phone", "", "phone number (optional; mutually exclusive with -email)") + email := flag.String("email", "", "email address for an email-signup account (optional; mutually exclusive with -phone)") + force := flag.Bool("force", false, "skip the reserved-id / sequence-collision safety checks") + flag.Parse() + + if *id <= 0 { + fmt.Fprintln(os.Stderr, "createuser: -id is required and must be positive") + os.Exit(2) + } + if *phone != "" && *email != "" { + fmt.Fprintln(os.Stderr, "createuser: -phone and -email are mutually exclusive") + os.Exit(2) + } + if !*force { + if domain.IsSystemUserID(*id) { + fmt.Fprintf(os.Stderr, "createuser: %d is a reserved built-in system account id (see internal/domain/system.go) - refusing, pass -force to override\n", *id) + os.Exit(2) + } + if *id >= domain.UserIDSequenceBase { + fmt.Fprintf(os.Stderr, "createuser: %d is >= UserIDSequenceBase (%d) - a future organic signup could eventually collide with it; pass -force to proceed anyway (then consider bumping users_id_seq yourself)\n", *id, domain.UserIDSequenceBase) + os.Exit(2) + } + } + + cfg, err := config.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "createuser: load config: %v\n", err) + os.Exit(1) + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + pool, err := pgxpool.New(ctx, cfg.PostgresDSN) + if err != nil { + fmt.Fprintf(os.Stderr, "createuser: connect: %v\n", err) + os.Exit(1) + } + defer pool.Close() + + accessHash, err := randomInt64() + if err != nil { + fmt.Fprintf(os.Stderr, "createuser: %v\n", err) + os.Exit(1) + } + + displayPhone := *phone + signupEmail := "" + if *email != "" { + signupEmail = domain.NormalizeEmailForPhone(*email) + displayPhone, err = assignEmailSignupDisplayPhone(ctx, pool) + if err != nil { + fmt.Fprintf(os.Stderr, "createuser: %v\n", err) + os.Exit(1) + } + } + + // phone/username/signup_email all sit under partial unique indexes that + // exclude '', so leaving any of them blank never collides with another + // blank-valued account. + row := pool.QueryRow(ctx, ` + INSERT INTO users (id, access_hash, phone, signup_email, first_name, last_name, username, country_code) + VALUES ($1, $2, $3, $4, $5, $6, $7, '') + ON CONFLICT (id) DO NOTHING + RETURNING id`, + *id, accessHash, displayPhone, signupEmail, *firstName, *lastName, *username) + + var createdID int64 + if err := row.Scan(&createdID); err != nil { + fmt.Fprintln(os.Stderr, describeInsertFailure(*id, *username, displayPhone, signupEmail, err)) + os.Exit(1) + } + + fmt.Printf("created user id=%d access_hash=%d first_name=%q last_name=%q username=%q phone=%q signup_email=%q\n", + createdID, accessHash, *firstName, *lastName, *username, displayPhone, signupEmail) +} + +// describeInsertFailure turns the INSERT's failure into a message naming the +// actual thing that collided, instead of "id already exists" for every case: +// ON CONFLICT (id) DO NOTHING only covers the id itself, so a duplicate +// username/phone/signup_email surfaces here as a distinct unique-violation +// error (pgx.ErrNoRows only means the id itself was the conflict). +func describeInsertFailure(id int64, username, phone, signupEmail string, err error) string { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23505" { + switch pgErr.ConstraintName { + case "users_username_lower_unique_idx": + return fmt.Sprintf("createuser: username %q is already taken", username) + case "users_phone_unique_idx": + return fmt.Sprintf("createuser: phone %q is already in use", phone) + case "users_signup_email_lower_unique_idx": + return fmt.Sprintf("createuser: email %q is already in use by another account", signupEmail) + default: + return fmt.Sprintf("createuser: unique constraint %q violated: %v", pgErr.ConstraintName, err) + } + } + if errors.Is(err, pgx.ErrNoRows) { + return fmt.Sprintf("createuser: id %d already exists", id) + } + return fmt.Sprintf("createuser: insert failed: %v", err) +} + +// assignEmailSignupDisplayPhone mirrors internal/app/auth/service.go's method +// of the same name: pick a random "888"-prefixed display phone and re-roll on +// the astronomically unlikely collision with an existing account's phone. +func assignEmailSignupDisplayPhone(ctx context.Context, pool *pgxpool.Pool) (string, error) { + for range maxEmailSignupPhoneAttempts { + candidate, err := domain.NewEmailSignupDisplayPhone(domain.EmailPhonePrefix) + if err != nil { + return "", err + } + var exists bool + if err := pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM users WHERE phone = $1)`, candidate).Scan(&exists); err != nil { + return "", fmt.Errorf("check display phone collision: %w", err) + } + if !exists { + return candidate, nil + } + } + return "", fmt.Errorf("assign email signup display phone: exhausted %d attempts", maxEmailSignupPhoneAttempts) +} diff --git a/cmd/telesrv-admin/readstore.go b/cmd/telesrv-admin/readstore.go index df718903..4c78b3b6 100644 --- a/cmd/telesrv-admin/readstore.go +++ b/cmd/telesrv-admin/readstore.go @@ -837,18 +837,21 @@ WITH auth AS ( SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.updated_at, COALESCE(r.frozen, false), COALESCE(r.reason, ''), u.verified, u.scam, u.fake, COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint, - auth.last_active_at, auth.device_count, + COALESCE(auth.last_active_at, '0001-01-01 00:00:00+00'::timestamptz), COALESCE(auth.device_count, 0)::int, COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username, COALESCE(ap.login_email, ''), `+accountCollectibleUsernamesColumn+` AS collectibles FROM users u -JOIN auth ON auth.user_id = u.id +-- LEFT JOIN, not JOIN: an account with no authorizations (never finished login, +-- all sessions revoked, frozen-then-unfrozen) must still appear here, matching +-- CountAccounts and SearchAccounts. +LEFT JOIN auth ON auth.user_id = u.id LEFT JOIN account_restrictions r ON r.user_id = u.id LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable LEFT JOIN account_passwords ap ON ap.user_id = u.id WHERE NOT u.is_bot - AND ($1::bigint = 0 OR (auth.last_active_at, u.id) < (to_timestamp(($1::double precision) / 1000000.0), $2::bigint)) -ORDER BY auth.last_active_at DESC, u.id DESC + AND ($1::bigint = 0 OR (COALESCE(auth.last_active_at, '0001-01-01 00:00:00+00'::timestamptz), u.id) < (to_timestamp(($1::double precision) / 1000000.0), $2::bigint)) +ORDER BY COALESCE(auth.last_active_at, '0001-01-01 00:00:00+00'::timestamptz) DESC, u.id DESC LIMIT $3`, beforeActiveUS, beforeID, limit+1) if err != nil { return nil, false, fmt.Errorf("list accounts: %w", err) diff --git a/cmd/telesrv-admin/readstore_accounts_integration_test.go b/cmd/telesrv-admin/readstore_accounts_integration_test.go index 59db4ee6..94f65e27 100644 --- a/cmd/telesrv-admin/readstore_accounts_integration_test.go +++ b/cmd/telesrv-admin/readstore_accounts_integration_test.go @@ -47,8 +47,8 @@ VALUES ($1, $2, $3, 'Collector', '', $4, now(), now())`, userID, userID, "+1889"+suffix, editable); err != nil { t.Fatalf("seed user: %v", err) } - // The list query joins authorizations, so an account with no device never - // appears there at all; an authorization in turn needs its auth key to exist. + // Give this account a device so its device_count / last_active columns are + // exercised; an authorization needs its auth key to exist first. if _, err := pool.Exec(ctx, ` INSERT INTO auth_keys (auth_key_id, body, server_salt) VALUES ($1, '\x00', 0)`, userID); err != nil { t.Fatalf("seed auth key: %v", err) @@ -123,6 +123,44 @@ WHERE peer_type='user' AND peer_id=$1 AND collectible_id IS NOT NULL`, userID); } } +// An account with no authorizations (never finished login, all sessions revoked, +// frozen-then-unfrozen) must still show up in the Accounts tab - it did not, +// because ListAccounts inner-joined the authorizations aggregate. +func TestReadStoreListAccountsIncludesAccountsWithoutSessions(t *testing.T) { + store, pool := verificationReadStore(t) + ctx := context.Background() + suffix := fmt.Sprintf("%d", time.Now().UnixNano()%1_000_000) + userID := 3_700_000_000 + time.Now().UnixNano()%1_000_000 + + t.Cleanup(func() { + _, _ = pool.Exec(ctx, `DELETE FROM users WHERE id=$1`, userID) + }) + if _, err := pool.Exec(ctx, ` +INSERT INTO users (id, access_hash, phone, first_name, last_name, username, created_at, updated_at) +VALUES ($1, $2, $3, 'Sessionless', '', '', now(), now())`, + userID, userID, "+42777"+suffix); err != nil { + t.Fatalf("seed user: %v", err) + } + // Deliberately no auth_keys / authorizations rows. + + rows, _, err := store.ListAccounts(ctx, 0, 0, 500) + if err != nil { + t.Fatalf("ListAccounts: %v", err) + } + found := false + for i := range rows { + if rows[i].ID == userID { + found = true + if rows[i].DeviceCount != 0 { + t.Fatalf("device count = %d, want 0 for a sessionless account", rows[i].DeviceCount) + } + } + } + if !found { + t.Fatalf("sessionless account %d absent from ListAccounts (%d rows)", userID, len(rows)) + } +} + func assertCollectibles(t *testing.T, surface string, row AccountRow, editable string, want []AccountUsername) { t.Helper() if row.Username != editable { diff --git a/cmd/telesrv-admin/server.go b/cmd/telesrv-admin/server.go index 35683331..f61f3626 100644 --- a/cmd/telesrv-admin/server.go +++ b/cmd/telesrv-admin/server.go @@ -12,6 +12,7 @@ import ( "io/fs" "mime/multipart" "net/http" + "net/url" "path" "strconv" "strings" @@ -74,6 +75,7 @@ func (s *server) routes() http.Handler { mux.Handle("GET /api/messages/groups", s.requireAuthAPI(http.HandlerFunc(s.handleGroupMessagesAPI))) mux.Handle("GET /api/messages/groups/detail", s.requireAuthAPI(http.HandlerFunc(s.handleGroupMessageDetailAPI))) mux.Handle("GET /api/collectible-usernames", s.requireAuthAPI(http.HandlerFunc(s.handleCollectibleUsernamesAPI))) + mux.Handle("GET /api/reserved-usernames", s.requireAuthAPI(http.HandlerFunc(s.handleReservedUsernamesAPI))) mux.Handle("GET /api/collectible-usernames/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleCollectibleUsernameDetailAPI))) mux.Handle("GET /api/storage/stats", s.requireAuthAPI(http.HandlerFunc(s.handleStorageStatsAPI))) mux.Handle("GET /api/storage/accounts", s.requireAuthAPI(http.HandlerFunc(s.handleStorageAccountsAPI))) @@ -128,6 +130,8 @@ func (s *server) routes() http.Handler { mux.Handle("POST /api/actions/auto-categorize-gif-catalog", s.requireAuthAPI(http.HandlerFunc(s.handleAutoCategorizeGifCatalogAPI))) mux.Handle("POST /api/actions/delete-uncategorized-gifs", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteUncategorizedGifsAPI))) mux.Handle("POST /api/actions/delete-gif-catalog-entry", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteGifCatalogEntryAPI))) + mux.Handle("POST /api/actions/reserve-username", s.requireAuthAPI(http.HandlerFunc(s.handleReserveUsernameAPI))) + mux.Handle("POST /api/actions/unreserve-username", s.requireAuthAPI(http.HandlerFunc(s.handleUnreserveUsernameAPI))) mux.Handle("POST /api/actions/mint-collectible-username", s.requireAuthAPI(http.HandlerFunc(s.handleMintCollectibleUsernameAPI))) mux.Handle("POST /api/actions/transfer-collectible-username", s.requireAuthAPI(http.HandlerFunc(s.handleTransferCollectibleUsernameAPI))) mux.Handle("POST /api/actions/revoke-collectible-username", s.requireAuthAPI(http.HandlerFunc(s.handleRevokeCollectibleUsernameAPI))) @@ -2239,6 +2243,69 @@ type mintCollectibleUsernameAPIRequest struct { PurchaseDate flexUnix `json:"purchase_date"` } +type reserveUsernameAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + Username string `json:"username"` +} + +func (s *server) handleReserveUsernameAPI(w http.ResponseWriter, r *http.Request) { + var body reserveUsernameAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.ReserveUsernameRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "reserve-username"), + Username: body.Username, + } + result, err := s.callAdminAPI(r.Context(), "/v1/reserved-usernames/reserve", req) + writeCommandResultAPI(w, result, err) +} + +func (s *server) handleUnreserveUsernameAPI(w http.ResponseWriter, r *http.Request) { + var body reserveUsernameAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.UnreserveUsernameRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "unreserve-username"), + Username: body.Username, + } + result, err := s.callAdminAPI(r.Context(), "/v1/reserved-usernames/unreserve", req) + writeCommandResultAPI(w, result, err) +} + +func (s *server) handleReservedUsernamesAPI(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + params := url.Values{} + for _, name := range []string{"q", "limit", "offset"} { + if v := strings.TrimSpace(q.Get(name)); v != "" { + params.Set(name, v) + } + } + apiPath := "/v1/reserved-usernames" + if enc := params.Encode(); enc != "" { + apiPath += "?" + enc + } + req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, s.cfg.AdminAPIURL+apiPath, nil) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, "request build failed") + return + } + req.Header.Set("Authorization", "Bearer "+s.cfg.AdminAPIToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + writeAPIError(w, http.StatusBadGateway, "admin api unreachable") + return + } + defer resp.Body.Close() + data, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.StatusCode) + _, _ = w.Write(data) +} + func (s *server) handleMintCollectibleUsernameAPI(w http.ResponseWriter, r *http.Request) { var body mintCollectibleUsernameAPIRequest if !decodeAction(w, r, &body) { diff --git a/cmd/telesrv-admin/web/dist/assets/index-Bt9UBcEE.js b/cmd/telesrv-admin/web/dist/assets/index-Bt9UBcEE.js deleted file mode 100644 index 8df9b38b..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-Bt9UBcEE.js +++ /dev/null @@ -1,9 +0,0 @@ -var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.provider`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.iterator;function p(e){return typeof e!=`object`||!e?null:(e=f&&e[f]||e[`@@iterator`],typeof e==`function`?e:null)}var m={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},h=Object.assign,g={};function _(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}_.prototype.isReactComponent={},_.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`setState(...): takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},_.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function v(){}v.prototype=_.prototype;function y(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}var b=y.prototype=new v;b.constructor=y,h(b,_.prototype),b.isPureReactComponent=!0;var x=Array.isArray,S=Object.prototype.hasOwnProperty,C={current:null},w={key:!0,ref:!0,__self:!0,__source:!0};function T(e,n,r){var i,a={},o=null,s=null;if(n!=null)for(i in n.ref!==void 0&&(s=n.ref),n.key!==void 0&&(o=``+n.key),n)S.call(n,i)&&!w.hasOwnProperty(i)&&(a[i]=n[i]);var c=arguments.length-2;if(c===1)a.children=r;else if(1{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=typeof setTimeout==`function`?setTimeout:null,_=typeof clearTimeout==`function`?clearTimeout:null,v=typeof setImmediate<`u`?setImmediate:null;typeof navigator<`u`&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function y(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function b(e){if(h=!1,y(e),!m)if(n(c)!==null)m=!0,M(x);else{var t=n(l);t!==null&&N(b,t.startTime-e)}}function x(t,i){m=!1,h&&(h=!1,_(w),w=-1),p=!0;var a=f;try{for(y(i),d=n(c);d!==null&&(!(d.expirationTime>i)||t&&!D());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=i);i=e.unstable_now(),typeof s==`function`?d.callback=s:d===n(c)&&r(c),y(i)}else r(c);d=n(c)}if(d!==null)var u=!0;else{var g=n(l);g!==null&&N(b,g.startTime-i),u=!1}return u}finally{d=null,f=a,p=!1}}var S=!1,C=null,w=-1,T=5,E=-1;function D(){return!(e.unstable_now()-Ee||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(_(w),w=-1):h=!0,N(b,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,M(x))),r},e.unstable_shouldYield=D,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u(),n=f();function r(e){for(var t=`https://reactjs.org/docs/error-decoder.html?invariant=`+e,n=1;n`u`||window.document===void 0||window.document.createElement===void 0),l=Object.prototype.hasOwnProperty,d=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,p={},m={};function h(e){return l.call(m,e)?!0:l.call(p,e)?!1:d.test(e)?m[e]=!0:(p[e]=!0,!1)}function g(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case`function`:case`symbol`:return!0;case`boolean`:return r?!1:n===null?(e=e.toLowerCase().slice(0,5),e!==`data-`&&e!==`aria-`):!n.acceptsBooleans;default:return!1}}function _(e,t,n,r){if(t==null||g(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return!1===t;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function v(e,t,n,r,i,a,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=i,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=o}var y={};`children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style`.split(` `).forEach(function(e){y[e]=new v(e,0,!1,e,null,!1,!1)}),[[`acceptCharset`,`accept-charset`],[`className`,`class`],[`htmlFor`,`for`],[`httpEquiv`,`http-equiv`]].forEach(function(e){var t=e[0];y[t]=new v(t,1,!1,e[1],null,!1,!1)}),[`contentEditable`,`draggable`,`spellCheck`,`value`].forEach(function(e){y[e]=new v(e,2,!1,e.toLowerCase(),null,!1,!1)}),[`autoReverse`,`externalResourcesRequired`,`focusable`,`preserveAlpha`].forEach(function(e){y[e]=new v(e,2,!1,e,null,!1,!1)}),`allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope`.split(` `).forEach(function(e){y[e]=new v(e,3,!1,e.toLowerCase(),null,!1,!1)}),[`checked`,`multiple`,`muted`,`selected`].forEach(function(e){y[e]=new v(e,3,!0,e,null,!1,!1)}),[`capture`,`download`].forEach(function(e){y[e]=new v(e,4,!1,e,null,!1,!1)}),[`cols`,`rows`,`size`,`span`].forEach(function(e){y[e]=new v(e,6,!1,e,null,!1,!1)}),[`rowSpan`,`start`].forEach(function(e){y[e]=new v(e,5,!1,e.toLowerCase(),null,!1,!1)});var b=/[\-:]([a-z])/g;function x(e){return e[1].toUpperCase()}`accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,null,!1,!1)}),`xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/1999/xlink`,!1,!1)}),[`xml:base`,`xml:lang`,`xml:space`].forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/XML/1998/namespace`,!1,!1)}),[`tabIndex`,`crossOrigin`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!1,!1)}),y.xlinkHref=new v(`xlinkHref`,1,!1,`xlink:href`,`http://www.w3.org/1999/xlink`,!0,!1),[`src`,`href`,`action`,`formAction`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!0,!0)});function S(e,t,n,r){var i=y.hasOwnProperty(t)?y[t]:null;(i===null?r||!(2s||i[o]!==a[s]){var c=` -`+i[o].replace(` at new `,` at `);return e.displayName&&c.includes(``)&&(c=c.replace(``,e.displayName)),c}while(1<=o&&0<=s);break}}}finally{ae=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:``)?ie(e):``}function se(e){switch(e.tag){case 5:return ie(e.type);case 16:return ie(`Lazy`);case 13:return ie(`Suspense`);case 19:return ie(`SuspenseList`);case 0:case 2:case 15:return e=oe(e.type,!1),e;case 11:return e=oe(e.type.render,!1),e;case 1:return e=oe(e.type,!0),e;default:return``}}function ce(e){if(e==null)return null;if(typeof e==`function`)return e.displayName||e.name||null;if(typeof e==`string`)return e;switch(e){case E:return`Fragment`;case T:return`Portal`;case O:return`Profiler`;case D:return`StrictMode`;case M:return`Suspense`;case N:return`SuspenseList`}if(typeof e==`object`)switch(e.$$typeof){case A:return(e.displayName||`Context`)+`.Consumer`;case k:return(e._context.displayName||`Context`)+`.Provider`;case j:var t=e.render;return e=e.displayName,e||=(e=t.displayName||t.name||``,e===``?`ForwardRef`:`ForwardRef(`+e+`)`),e;case P:return t=e.displayName||null,t===null?ce(e.type)||`Memo`:t;case ee:t=e._payload,e=e._init;try{return ce(e(t))}catch{}}return null}function le(e){var t=e.type;switch(e.tag){case 24:return`Cache`;case 9:return(t.displayName||`Context`)+`.Consumer`;case 10:return(t._context.displayName||`Context`)+`.Provider`;case 18:return`DehydratedFragment`;case 11:return e=t.render,e=e.displayName||e.name||``,t.displayName||(e===``?`ForwardRef`:`ForwardRef(`+e+`)`);case 7:return`Fragment`;case 5:return t;case 4:return`Portal`;case 3:return`Root`;case 6:return`Text`;case 16:return ce(t);case 8:return t===D?`StrictMode`:`Mode`;case 22:return`Offscreen`;case 12:return`Profiler`;case 21:return`Scope`;case 13:return`Suspense`;case 19:return`SuspenseList`;case 25:return`TracingMarker`;case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t==`function`)return t.displayName||t.name||null;if(typeof t==`string`)return t}return null}function ue(e){switch(typeof e){case`boolean`:case`number`:case`string`:case`undefined`:return e;case`object`:return e;default:return``}}function de(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()===`input`&&(t===`checkbox`||t===`radio`)}function fe(e){var t=de(e)?`checked`:`value`,n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=``+e[t];if(!e.hasOwnProperty(t)&&n!==void 0&&typeof n.get==`function`&&typeof n.set==`function`){var i=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(e){r=``+e,a.call(this,e)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(e){r=``+e},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function L(e){e._valueTracker||=fe(e)}function pe(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r=``;return e&&(r=de(e)?e.checked?`true`:`false`:e.value),e=r,e===n?!1:(t.setValue(e),!0)}function me(e){if(e||=typeof document<`u`?document:void 0,e===void 0)return null;try{return e.activeElement||e.body}catch{return e.body}}function he(e,t){var n=t.checked;return I({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function ge(e,t){var n=t.defaultValue==null?``:t.defaultValue,r=t.checked==null?t.defaultChecked:t.checked;n=ue(t.value==null?n:t.value),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type===`checkbox`||t.type===`radio`?t.checked!=null:t.value!=null}}function _e(e,t){t=t.checked,t!=null&&S(e,`checked`,t,!1)}function ve(e,t){_e(e,t);var n=ue(t.value),r=t.type;if(n!=null)r===`number`?(n===0&&e.value===``||e.value!=n)&&(e.value=``+n):e.value!==``+n&&(e.value=``+n);else if(r===`submit`||r===`reset`){e.removeAttribute(`value`);return}t.hasOwnProperty(`value`)?be(e,t.type,n):t.hasOwnProperty(`defaultValue`)&&be(e,t.type,ue(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ye(e,t,n){if(t.hasOwnProperty(`value`)||t.hasOwnProperty(`defaultValue`)){var r=t.type;if(!(r!==`submit`&&r!==`reset`||t.value!==void 0&&t.value!==null))return;t=``+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==``&&(e.name=``),e.defaultChecked=!!e._wrapperState.initialChecked,n!==``&&(e.name=n)}function be(e,t,n){(t!==`number`||me(e.ownerDocument)!==e)&&(n==null?e.defaultValue=``+e._wrapperState.initialValue:e.defaultValue!==``+n&&(e.defaultValue=``+n))}var xe=Array.isArray;function Se(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i`+t.valueOf().toString()+``,t=Oe.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Ae(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var je={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Me=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(je).forEach(function(e){Me.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),je[t]=je[e]})});function Ne(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||je.hasOwnProperty(e)&&je[e]?(``+t).trim():t+`px`}function Pe(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=Ne(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var Fe=I({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Ie(e,t){if(t){if(Fe[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(r(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(r(60));if(typeof t.dangerouslySetInnerHTML!=`object`||!(`__html`in t.dangerouslySetInnerHTML))throw Error(r(61))}if(t.style!=null&&typeof t.style!=`object`)throw Error(r(62))}}function Le(e,t){if(e.indexOf(`-`)===-1)return typeof t.is==`string`;switch(e){case`annotation-xml`:case`color-profile`:case`font-face`:case`font-face-src`:case`font-face-uri`:case`font-face-format`:case`font-face-name`:case`missing-glyph`:return!1;default:return!0}}var Re=null;function ze(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Be=null,Ve=null,He=null;function Ue(e){if(e=Ai(e)){if(typeof Be!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Mi(t),Be(e.stateNode,e.type,t))}}function We(e){Ve?He?He.push(e):He=[e]:Ve=e}function Ge(){if(Ve){var e=Ve,t=He;if(He=Ve=null,Ue(e),t)for(e=0;e>>=0,e===0?32:31-(Ct(e)/wt|0)|0}var Et=64,W=4194304;function Dt(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ot(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,i=e.suspendedLanes,a=e.pingedLanes,o=n&268435455;if(o!==0){var s=o&~i;s===0?(a&=o,a!==0&&(r=Dt(a))):r=Dt(s)}else o=n&~i,o===0?a!==0&&(r=Dt(a)):r=Dt(o);if(r===0)return 0;if(t!==0&&t!==r&&(t&i)===0&&(i=r&-r,a=t&-t,i>=a||i===16&&a&4194240))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Y(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-St(t),e[t]=n}function At(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Wn),qn=` `,Jn=!1;function Yn(e,t){switch(e){case`keyup`:return Hn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Xn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Zn=!1;function Qn(e,t){switch(e){case`compositionend`:return Xn(t);case`keypress`:return t.which===32?(Jn=!0,qn):null;case`textInput`:return e=t.data,e===qn&&Jn?null:e;default:return null}}function $n(e,t){if(Zn)return e===`compositionend`||!Un&&Yn(e,t)?(e=pn(),fn=dn=un=null,Zn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=br(n)}}function Sr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Sr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Cr(){for(var e=window,t=me();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=me(e.document)}return t}function wr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}function Tr(e){var t=Cr(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Sr(n.ownerDocument.documentElement,n)){if(r!==null&&wr(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),`selectionStart`in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=n.textContent.length,a=Math.min(r.start,i);r=r.end===void 0?a:Math.min(r.end,i),!e.extend&&a>r&&(i=r,r=a,a=i),i=xr(n,a);var o=xr(n,r);i&&o&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus==`function`&&n.focus(),n=0;n=document.documentMode,Dr=null,Or=null,kr=null,Ar=!1;function jr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ar||Dr==null||Dr!==me(r)||(r=Dr,`selectionStart`in r&&wr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),kr&&yr(kr,r)||(kr=r,r=ri(Or,`onSelect`),0Pi||(e.current=Ni[Pi],Ni[Pi]=null,Pi--)}function Li(e,t){Pi++,Ni[Pi]=e.current,e.current=t}var Ri={},zi=Fi(Ri),Bi=Fi(!1),Vi=Ri;function Hi(e,t){var n=e.type.contextTypes;if(!n)return Ri;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},a;for(a in n)i[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Ui(e){return e=e.childContextTypes,e!=null}function Wi(){Ii(Bi),Ii(zi)}function Gi(e,t,n){if(zi.current!==Ri)throw Error(r(168));Li(zi,t),Li(Bi,n)}function Ki(e,t,n){var i=e.stateNode;if(t=t.childContextTypes,typeof i.getChildContext!=`function`)return n;for(var a in i=i.getChildContext(),i)if(!(a in t))throw Error(r(108,le(e)||`Unknown`,a));return I({},n,i)}function qi(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ri,Vi=zi.current,Li(zi,e),Li(Bi,Bi.current),!0}function Ji(e,t,n){var i=e.stateNode;if(!i)throw Error(r(169));n?(e=Ki(e,t,Vi),i.__reactInternalMemoizedMergedChildContext=e,Ii(Bi),Ii(zi),Li(zi,e)):Ii(Bi),Li(Bi,n)}var Yi=null,Xi=!1,Zi=!1;function Qi(e){Yi===null?Yi=[e]:Yi.push(e)}function $i(e){Xi=!0,Qi(e)}function ea(){if(!Zi&&Yi!==null){Zi=!0;var e=0,t=X;try{var n=Yi;for(X=1;e>=o,i-=o,ca=1<<32-St(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(r,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(r,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(r,d),ga&&ua(r,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),ga&&ua(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return ga&&ua(a,g),u}for(h=i(a,h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),ga&&ua(a,g),u}function _(e,r,i,o){if(typeof i==`object`&&i&&i.type===E&&i.key===null&&(i=i.props.children),typeof i==`object`&&i){switch(i.$$typeof){case w:a:{for(var c=i.key,l=r;l!==null;){if(l.key===c){if(c=i.type,c===E){if(l.tag===7){n(e,l.sibling),r=a(l,i.props.children),r.return=e,e=r;break a}}else if(l.elementType===c||typeof c==`object`&&c&&c.$$typeof===ee&&Aa(c)===l.type){n(e,l.sibling),r=a(l,i.props),r.ref=Oa(e,l,i),r.return=e,e=r;break a}n(e,l);break}else t(e,l);l=l.sibling}i.type===E?(r=Zl(i.props.children,e.mode,o,i.key),r.return=e,e=r):(o=Xl(i.type,i.key,i.props,null,e.mode,o),o.ref=Oa(e,r,i),o.return=e,e=o)}return s(e);case T:a:{for(l=i.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===i.containerInfo&&r.stateNode.implementation===i.implementation){n(e,r.sibling),r=a(r,i.children||[]),r.return=e,e=r;break a}else{n(e,r);break}else t(e,r);r=r.sibling}r=eu(i,e.mode,o),r.return=e,e=r}return s(e);case ee:return l=i._init,_(e,r,l(i._payload),o)}if(xe(i))return h(e,r,i,o);if(ne(i))return g(e,r,i,o);ka(e,i)}return typeof i==`string`&&i!==``||typeof i==`number`?(i=``+i,r!==null&&r.tag===6?(n(e,r.sibling),r=a(r,i),r.return=e,e=r):(n(e,r),r=$l(i,e.mode,o),r.return=e,e=r),s(e)):n(e,r)}return _}var Ma=ja(!0),Na=ja(!1),Pa=Fi(null),Fa=null,Ia=null,La=null;function Ra(){La=Ia=Fa=null}function za(e){var t=Pa.current;Ii(Pa),e._currentValue=t}function Ba(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)===t?r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t):(e.childLanes|=t,r!==null&&(r.childLanes|=t)),e===n)break;e=e.return}}function Va(e,t){Fa=e,La=Ia=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(js=!0),e.firstContext=null)}function Ha(e){var t=e._currentValue;if(La!==e)if(e={context:e,memoizedValue:t,next:null},Ia===null){if(Fa===null)throw Error(r(308));Ia=e,Fa.dependencies={lanes:0,firstContext:e}}else Ia=Ia.next=e;return t}var Ua=null;function Wa(e){Ua===null?Ua=[e]:Ua.push(e)}function Ga(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,Wa(t)):(n.next=i.next,i.next=n),t.interleaved=n,Ka(e,r)}function Ka(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var qa=!1;function Ja(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ya(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Xa(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Za(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Bc&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,Ka(e,n)}return i=r.interleaved,i===null?(t.next=t,Wa(r)):(t.next=i.next,i.next=t),r.interleaved=t,Ka(e,n)}function Qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194240)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}function $a(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function eo(e,t,n,r){var i=e.updateQueue;qa=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane,p=s.eventTime;if((r&f)===f){u!==null&&(u=u.next={eventTime:p,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});a:{var m=e,h=s;switch(f=t,p=n,h.tag){case 1:if(m=h.payload,typeof m==`function`){d=m.call(p,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=h.payload,f=typeof m==`function`?m.call(p,d,f):m,f==null)break a;d=I({},d,f);break a;case 2:qa=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,f=i.effects,f===null?i.effects=[s]:f.push(s))}else p={eventTime:p,lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;f=s,s=f.next,f.next=null,i.lastBaseUpdate=f,i.shared.pending=null}}while(1);if(u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else a===null&&(i.shared.lanes=0);Jc|=o,e.lanes=o,e.memoizedState=d}}function to(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=_o.transition;_o.transition={};try{e(!1),t()}finally{X=n,_o.transition=r}}function is(){return jo().memoizedState}function as(e,t,n){var r=pl(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},ss(e))cs(t,n);else if(n=Ga(e,t,n,r),n!==null){var i=fl();ml(n,e,r,i),ls(n,t,r)}}function os(e,t,n){var r=pl(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(ss(e))cs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Q(s,o)){var c=t.interleaved;c===null?(i.next=i,Wa(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}n=Ga(e,t,i,r),n!==null&&(i=fl(),ml(n,e,r,i),ls(n,t,r))}}function ss(e){var t=e.alternate;return e===yo||t!==null&&t===yo}function cs(e,t){Co=So=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function ls(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}var us={readContext:Ha,useCallback:Eo,useContext:Eo,useEffect:Eo,useImperativeHandle:Eo,useInsertionEffect:Eo,useLayoutEffect:Eo,useMemo:Eo,useReducer:Eo,useRef:Eo,useState:Eo,useDebugValue:Eo,useDeferredValue:Eo,useTransition:Eo,useMutableSource:Eo,useSyncExternalStore:Eo,useId:Eo,unstable_isNewReconciler:!1},ds={readContext:Ha,useCallback:function(e,t){return Ao().memoizedState=[e,t===void 0?null:t],e},useContext:Ha,useEffect:qo,useImperativeHandle:function(e,t,n){return n=n==null?null:n.concat([e]),Go(4194308,4,Zo.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Go(4194308,4,e,t)},useInsertionEffect:function(e,t){return Go(4,2,e,t)},useMemo:function(e,t){var n=Ao();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ao();return t=n===void 0?t:n(t),r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=as.bind(null,yo,e),[r.memoizedState,e]},useRef:function(e){var t=Ao();return e={current:e},t.memoizedState=e},useState:Ho,useDebugValue:$o,useDeferredValue:function(e){return Ao().memoizedState=e},useTransition:function(){var e=Ho(!1),t=e[0];return e=rs.bind(null,e[1]),Ao().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var i=yo,a=Ao();if(ga){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Vc===null)throw Error(r(349));vo&30||Lo(i,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,qo(zo.bind(null,i,o,e),[e]),i.flags|=2048,Uo(9,Ro.bind(null,i,o,n,t),void 0,null),n},useId:function(){var e=Ao(),t=Vc.identifierPrefix;if(ga){var n=la,r=ca;n=(r&~(1<<32-St(r)-1)).toString(32)+n,t=`:`+t+`R`+n,n=wo++,0<\/script>`,e=e.removeChild(e.firstChild)):typeof i.is==`string`?e=c.createElement(n,{is:i.is}):(e=c.createElement(n),n===`select`&&(c=e,i.multiple?c.multiple=!0:i.size&&(c.size=i.size))):e=c.createElementNS(e,n),e[Ci]=t,e[wi]=i,tc(e,t,!1,!1),t.stateNode=e;a:{switch(c=Le(n,i),n){case`dialog`:Xr(`cancel`,e),Xr(`close`,e),o=i;break;case`iframe`:case`object`:case`embed`:Xr(`load`,e),o=i;break;case`video`:case`audio`:for(o=0;oel&&(t.flags|=128,i=!0,ic(s,!1),t.lanes=4194304)}else{if(!i)if(e=po(c),e!==null){if(t.flags|=128,i=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),ic(s,!0),s.tail===null&&s.tailMode===`hidden`&&!c.alternate&&!ga)return ac(t),null}else 2*pt()-s.renderingStartTime>el&&n!==1073741824&&(t.flags|=128,i=!0,ic(s,!1),t.lanes=4194304);s.isBackwards?(c.sibling=t.child,t.child=c):(n=s.last,n===null?t.child=c:n.sibling=c,s.last=c)}return s.tail===null?(ac(t),null):(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=pt(),t.sibling=null,n=fo.current,Li(fo,i?n&1|2:n&1),t);case 22:case 23:return wl(),i=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==i&&(t.flags|=8192),i&&t.mode&1?Wc&1073741824&&(ac(t),t.subtreeFlags&6&&(t.flags|=8192)):ac(t),null;case 24:return null;case 25:return null}throw Error(r(156,t.tag))}function sc(e,t){switch(pa(t),t.tag){case 1:return Ui(t.type)&&Wi(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return co(),Ii(Bi),Ii(zi),ho(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return uo(t),null;case 13:if(Ii(fo),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));Ta()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Ii(fo),null;case 4:return co(),null;case 10:return za(t.type._context),null;case 22:case 23:return wl(),null;case 24:return null;default:return null}}var cc=!1,lc=!1,uc=typeof WeakSet==`function`?WeakSet:Set,$=null;function dc(e,t){var n=e.ref;if(n!==null)if(typeof n==`function`)try{n(null)}catch(n){Rl(e,t,n)}else n.current=null}function fc(e,t,n){try{n()}catch(n){Rl(e,t,n)}}var pc=!1;function mc(e,t){if(di=rn,e=Cr(),wr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var i=n.getSelection&&n.getSelection();if(i&&i.rangeCount!==0){n=i.anchorNode;var a=i.anchorOffset,o=i.focusNode;i=i.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||i!==0&&f.nodeType!==3||(l=s+i),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===i&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(fi={focusedElem:e,selectionRange:n},rn=!1,$=t;$!==null;)if(t=$,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,$=e;else for(;$!==null;){t=$;try{var h=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(h!==null){var g=h.memoizedProps,_=h.memoizedState,v=t.stateNode;v.__reactInternalSnapshotBeforeUpdate=v.getSnapshotBeforeUpdate(t.elementType===t.type?g:ms(t.type,g),_)}break;case 3:var y=t.stateNode.containerInfo;y.nodeType===1?y.textContent=``:y.nodeType===9&&y.documentElement&&y.removeChild(y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(r(163))}}catch(e){Rl(t,t.return,e)}if(e=t.sibling,e!==null){e.return=t.return,$=e;break}$=t.return}return h=pc,pc=!1,h}function hc(e,t,n){var r=t.updateQueue;if(r=r===null?null:r.lastEffect,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var a=i.destroy;i.destroy=void 0,a!==void 0&&fc(t,n,a)}i=i.next}while(i!==r)}}function gc(e,t){if(t=t.updateQueue,t=t===null?null:t.lastEffect,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function _c(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t==`function`?t(e):t.current=e}}function vc(e){var t=e.alternate;t!==null&&(e.alternate=null,vc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ci],delete t[wi],delete t[Ei],delete t[Di],delete t[Oi])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function yc(e){return e.tag===5||e.tag===3||e.tag===4}function bc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||yc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function xc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=ui));else if(r!==4&&(e=e.child,e!==null))for(xc(e,t,n),e=e.sibling;e!==null;)xc(e,t,n),e=e.sibling}function Sc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Sc(e,t,n),e=e.sibling;e!==null;)Sc(e,t,n),e=e.sibling}var Cc=null,wc=!1;function Tc(e,t,n){for(n=n.child;n!==null;)Ec(e,t,n),n=n.sibling}function Ec(e,t,n){if(bt&&typeof bt.onCommitFiberUnmount==`function`)try{bt.onCommitFiberUnmount(yt,n)}catch{}switch(n.tag){case 5:lc||dc(n,t);case 6:var r=Cc,i=wc;Cc=null,Tc(e,t,n),Cc=r,wc=i,Cc!==null&&(wc?(e=Cc,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Cc.removeChild(n.stateNode));break;case 18:Cc!==null&&(wc?(e=Cc,n=n.stateNode,e.nodeType===8?yi(e.parentNode,n):e.nodeType===1&&yi(e,n),tn(e)):yi(Cc,n.stateNode));break;case 4:r=Cc,i=wc,Cc=n.stateNode.containerInfo,wc=!0,Tc(e,t,n),Cc=r,wc=i;break;case 0:case 11:case 14:case 15:if(!lc&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var a=i,o=a.destroy;a=a.tag,o!==void 0&&(a&2||a&4)&&fc(n,t,o),i=i.next}while(i!==r)}Tc(e,t,n);break;case 1:if(!lc&&(dc(n,t),r=n.stateNode,typeof r.componentWillUnmount==`function`))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(e){Rl(n,t,e)}Tc(e,t,n);break;case 21:Tc(e,t,n);break;case 22:n.mode&1?(lc=(r=lc)||n.memoizedState!==null,Tc(e,t,n),lc=r):Tc(e,t,n);break;default:Tc(e,t,n)}}function Dc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new uc),t.forEach(function(t){var r=Hl.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))})}}function Oc(e,t){var n=t.deletions;if(n!==null)for(var i=0;ia&&(a=s),i&=~o}if(i=a,i=pt()-i,i=(120>i?120:480>i?480:1080>i?1080:1920>i?1920:3e3>i?3e3:4320>i?4320:1960*Ic(i/1960))-i,10e?16:e,ol===null)var i=!1;else{if(e=ol,ol=null,sl=0,Bc&6)throw Error(r(331));var a=Bc;for(Bc|=4,$=e.current;$!==null;){var o=$,s=o.child;if($.flags&16){var c=o.deletions;if(c!==null){for(var l=0;lpt()-$c?Tl(e,0):Xc|=n),hl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=W,W<<=1,!(W&130023424)&&(W=4194304)):t=1);var n=fl();e=Ka(e,t),e!==null&&(Y(e,t,n),hl(e,n))}function Vl(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Bl(e,n)}function Hl(e,t){var n=0;switch(e.tag){case 13:var i=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:i=e.stateNode;break;default:throw Error(r(314))}i!==null&&i.delete(t),Bl(e,n)}var Ul=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Bi.current)js=!0;else{if((e.lanes&n)===0&&!(t.flags&128))return js=!1,ec(e,t,n);js=!!(e.flags&131072)}else js=!1,ga&&t.flags&1048576&&da(t,ia,t.index);switch(t.lanes=0,t.tag){case 2:var i=t.type;Qs(e,t),e=t.pendingProps;var a=Hi(t,zi.current);Va(t,n),a=Oo(null,t,i,e,a,n);var o=ko();return t.flags|=1,typeof a==`object`&&a&&typeof a.render==`function`&&a.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Ui(i)?(o=!0,qi(t)):o=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,Ja(t),a.updater=gs,t.stateNode=a,a._reactInternals=t,bs(t,i,e,n),t=Bs(null,t,i,!0,o,n)):(t.tag=0,ga&&o&&fa(t),Ms(null,t,a,n),t=t.child),t;case 16:i=t.elementType;a:{switch(Qs(e,t),e=t.pendingProps,a=i._init,i=a(i._payload),t.type=i,a=t.tag=Jl(i),e=ms(i,e),a){case 0:t=Rs(null,t,i,e,n);break a;case 1:t=zs(null,t,i,e,n);break a;case 11:t=Ns(null,t,i,e,n);break a;case 14:t=Ps(null,t,i,ms(i.type,e),n);break a}throw Error(r(306,i,``))}return t;case 0:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Rs(e,t,i,a,n);case 1:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),zs(e,t,i,a,n);case 3:a:{if(Vs(t),e===null)throw Error(r(387));i=t.pendingProps,o=t.memoizedState,a=o.element,Ya(e,t),eo(t,i,null,n);var s=t.memoizedState;if(i=s.element,o.isDehydrated)if(o={element:i,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){a=xs(Error(r(423)),t),t=Hs(e,t,i,n,a);break a}else if(i!==a){a=xs(Error(r(424)),t),t=Hs(e,t,i,n,a);break a}else for(ha=bi(t.stateNode.containerInfo.firstChild),ma=t,ga=!0,_a=null,n=Na(t,null,i,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Ta(),i===a){t=$s(e,t,n);break a}Ms(e,t,i,n)}t=t.child}return t;case 5:return lo(t),e===null&&xa(t),i=t.type,a=t.pendingProps,o=e===null?null:e.memoizedProps,s=a.children,pi(i,a)?s=null:o!==null&&pi(i,o)&&(t.flags|=32),Ls(e,t),Ms(e,t,s,n),t.child;case 6:return e===null&&xa(t),null;case 13:return Gs(e,t,n);case 4:return so(t,t.stateNode.containerInfo),i=t.pendingProps,e===null?t.child=Ma(t,null,i,n):Ms(e,t,i,n),t.child;case 11:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Ns(e,t,i,a,n);case 7:return Ms(e,t,t.pendingProps,n),t.child;case 8:return Ms(e,t,t.pendingProps.children,n),t.child;case 12:return Ms(e,t,t.pendingProps.children,n),t.child;case 10:a:{if(i=t.type._context,a=t.pendingProps,o=t.memoizedProps,s=a.value,Li(Pa,i._currentValue),i._currentValue=s,o!==null)if(Q(o.value,s)){if(o.children===a.children&&!Bi.current){t=$s(e,t,n);break a}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var c=o.dependencies;if(c!==null){s=o.child;for(var l=c.firstContext;l!==null;){if(l.context===i){if(o.tag===1){l=Xa(-1,n&-n),l.tag=2;var u=o.updateQueue;if(u!==null){u=u.shared;var d=u.pending;d===null?l.next=l:(l.next=d.next,d.next=l),u.pending=l}}o.lanes|=n,l=o.alternate,l!==null&&(l.lanes|=n),Ba(o.return,n,t),c.lanes|=n;break}l=l.next}}else if(o.tag===10)s=o.type===t.type?null:o.child;else if(o.tag===18){if(s=o.return,s===null)throw Error(r(341));s.lanes|=n,c=s.alternate,c!==null&&(c.lanes|=n),Ba(s,n,t),s=o.sibling}else s=o.child;if(s!==null)s.return=o;else for(s=o;s!==null;){if(s===t){s=null;break}if(o=s.sibling,o!==null){o.return=s.return,s=o;break}s=s.return}o=s}Ms(e,t,a.children,n),t=t.child}return t;case 9:return a=t.type,i=t.pendingProps.children,Va(t,n),a=Ha(a),i=i(a),t.flags|=1,Ms(e,t,i,n),t.child;case 14:return i=t.type,a=ms(i,t.pendingProps),a=ms(i.type,a),Ps(e,t,i,a,n);case 15:return Fs(e,t,t.type,t.pendingProps,n);case 17:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Qs(e,t),t.tag=1,Ui(i)?(e=!0,qi(t)):e=!1,Va(t,n),vs(t,i,a),bs(t,i,a,n),Bs(null,t,i,!0,e,n);case 19:return Zs(e,t,n);case 22:return Is(e,t,n)}throw Error(r(156,t.tag))};function Wl(e,t){return ut(e,t)}function Gl(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Kl(e,t,n,r){return new Gl(e,t,n,r)}function ql(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Jl(e){if(typeof e==`function`)return+!!ql(e);if(e!=null){if(e=e.$$typeof,e===j)return 11;if(e===P)return 14}return 2}function Yl(e,t){var n=e.alternate;return n===null?(n=Kl(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Xl(e,t,n,i,a,o){var s=2;if(i=e,typeof e==`function`)ql(e)&&(s=1);else if(typeof e==`string`)s=5;else a:switch(e){case E:return Zl(n.children,a,o,t);case D:s=8,a|=8;break;case O:return e=Kl(12,n,t,a|2),e.elementType=O,e.lanes=o,e;case M:return e=Kl(13,n,t,a),e.elementType=M,e.lanes=o,e;case N:return e=Kl(19,n,t,a),e.elementType=N,e.lanes=o,e;case te:return Ql(n,a,o,t);default:if(typeof e==`object`&&e)switch(e.$$typeof){case k:s=10;break a;case A:s=9;break a;case j:s=11;break a;case P:s=14;break a;case ee:s=16,i=null;break a}throw Error(r(130,e==null?e:typeof e,``))}return t=Kl(s,n,t,a),t.elementType=e,t.type=i,t.lanes=o,t}function Zl(e,t,n,r){return e=Kl(7,e,r,t),e.lanes=n,e}function Ql(e,t,n,r){return e=Kl(22,e,r,t),e.elementType=te,e.lanes=n,e.stateNode={isHidden:!1},e}function $l(e,t,n){return e=Kl(6,e,null,t),e.lanes=n,e}function eu(e,t,n){return t=Kl(4,e.children===null?[]:e.children,e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function tu(e,t,n,r,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=J(0),this.expirationTimes=J(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=J(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function nu(e,t,n,r,i,a,o,s,c){return e=new tu(e,t,n,s,c),t===1?(t=1,!0===a&&(t|=8)):t=0,a=Kl(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ja(a),e}function ru(e,t,n){var r=3{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=m();e.createRoot=t.createRoot,e.hydrateRoot=t.hydrateRoot})),g=c(u()),_=c(h(),1),v=class extends Error{status;constructor(e,t){super(t),this.status=e}},y=`telesrv_admin_csrf`,b=`X-CSRF-Token`,x=``;function S(e){x=(e??``).trim()}function C(){if(typeof document>`u`)return``;for(let e of document.cookie.split(`;`)){let t=e.trim(),n=t.indexOf(`=`);if(!(n<=0||t.slice(0,n)!==y))try{return decodeURIComponent(t.slice(n+1))}catch{return t.slice(n+1)}}return``}function w(){return C()||x}function T(e){let t=(e??`GET`).toUpperCase();return t!==`GET`&&t!==`HEAD`&&t!==`OPTIONS`}function E(e){if(!e)return{};if(e instanceof Headers){let t={};return e.forEach((e,n)=>{t[n]=e}),t}return Array.isArray(e)?Object.fromEntries(e):{...e}}async function D(e,t={}){let n=typeof FormData<`u`&&t.body instanceof FormData?{}:{"Content-Type":`application/json`};if(Object.assign(n,E(t.headers)),T(t.method)){let e=w();e&&(n[b]=e)}let r=await fetch(e,{credentials:`same-origin`,...t,headers:n}),i=await r.text(),a=i?JSON.parse(i):null;if(!r.ok){let e=a?.error||a?.Error||a?.message||r.statusText;throw new v(r.status,e)}return a}function O(e){return e instanceof Error?e.message:String(e)}var k={session:()=>D(`/api/session`),login:async e=>{let t=await D(`/api/login`,{method:`POST`,body:JSON.stringify({secret:e})});return S(t.csrf_token),t},logout:()=>D(`/api/logout`,{method:`POST`,body:`{}`}),accounts:e=>D(`/api/accounts?${e.toString()}`),accountStats:()=>D(`/api/accounts/stats`),sharedDeviceGroups:e=>D(`/api/accounts/shared-devices?${e.toString()}`),account:e=>D(`/api/accounts/${e}`),channels:e=>D(`/api/channels?${e.toString()}`),channel:e=>D(`/api/channels/${e}`),bots:e=>D(`/api/bots?${e.toString()}`),broadcasts:e=>D(`/api/broadcasts?${e.toString()}`),bot:e=>D(`/api/bots/${e}`),collectibleUsernames:e=>D(`/api/collectible-usernames?${e.toString()}`),collectibleUsername:e=>D(`/api/collectible-usernames/${encodeURIComponent(e)}`),dashboard:()=>D(`/api/dashboard`),storageStats:()=>D(`/api/storage/stats`),storageAccounts:e=>D(`/api/storage/accounts?${e.toString()}`),verificationApplications:e=>D(`/api/verification/applications?${e.toString()}`),verificationApplication:e=>D(`/api/verification/applications/${encodeURIComponent(e)}`),verificationCounts:()=>D(`/api/verification/counts`),botVerifiers:e=>D(`/api/botverification/verifiers?${e.toString()}`),verificationIcons:e=>D(`/api/botverification/icons?${e.toString()}`),customVerifications:e=>D(`/api/botverification/marks?${e.toString()}`),customVerificationRequests:e=>D(`/api/botverification/requests?${e.toString()}`),customVerificationRequest:e=>D(`/api/botverification/requests/${encodeURIComponent(e)}`),botVerificationCounts:()=>D(`/api/botverification/counts`),emoji:e=>D(`/api/emoji?${e.toString()}`),emojiAnimation:e=>D(`/api/emoji/${encodeURIComponent(e)}/animation`),messages:e=>D(`/api/messages?${e.toString()}`),message:(e,t)=>D(`/api/messages/detail?${new URLSearchParams({owner_user_id:String(e),msg_id:String(t)}).toString()}`),groupMessages:e=>D(`/api/messages/groups?${e.toString()}`),groupMessage:(e,t)=>D(`/api/messages/groups/detail?${new URLSearchParams({channel_id:String(e),msg_id:String(t)}).toString()}`),moderationCases:e=>D(`/api/moderation/cases?${e.toString()}`),moderationCase:e=>D(`/api/moderation/cases/${e}`),moderationReport:e=>D(`/api/moderation/reports/${e}`),claimModerationCase:(e,t)=>D(`/api/moderation/cases/${e}/claim`,{method:`POST`,body:JSON.stringify({expected_version:t})}),decideModerationCase:(e,t)=>D(`/api/moderation/cases/${e}/decide`,{method:`POST`,body:JSON.stringify(t)}),reviewModerationAppeal:(e,t,n)=>D(`/api/moderation/cases/${e}/appeals/${t}/review`,{method:`POST`,body:JSON.stringify(n)}),stickerSets:e=>D(`/api/stickers?kind=${encodeURIComponent(e)}`),stickerSetDocuments:e=>D(`/api/stickers/${encodeURIComponent(e)}/documents`),stickerDocumentAnimationURL:e=>`/api/stickers/documents/${encodeURIComponent(e)}/animation`,gifCatalogDocumentPreviewURL:e=>`/api/gif-catalog/documents/${encodeURIComponent(e)}/preview`,createStickerSet:e=>D(`/api/actions/create-sticker-set`,{method:`POST`,body:e}),setAccountAvatar:e=>D(`/api/actions/set-account-avatar`,{method:`POST`,body:e}),setChannelAvatar:e=>D(`/api/actions/set-channel-avatar`,{method:`POST`,body:e}),addStickerToSet:e=>D(`/api/actions/add-sticker-to-set`,{method:`POST`,body:e}),gifCatalog:()=>D(`/api/gif-catalog`),createGifCatalogEntry:e=>D(`/api/actions/create-gif-catalog-entry`,{method:`POST`,body:e}),action:(e,t)=>D(e,{method:`POST`,body:JSON.stringify(t)})},A=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),j=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),M={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},N=(0,g.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i=``,children:a,iconNode:o,...s},c)=>(0,g.createElement)(`svg`,{ref:c,...M,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:j(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),P=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(N,{ref:i,iconNode:t,className:j(`lucide-${A(e)}`,n),...r}));return n.displayName=`${e}`,n},ee=P(`BadgeCheck`,[[`path`,{d:`M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z`,key:`3c2336`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),te=P(`CircleAlert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),F=P(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),ne=P(`CircleX`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m15 9-6 6`,key:`1uzhvr`}],[`path`,{d:`m9 9 6 6`,key:`z0biqf`}]]),I=P(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),re=P(`ShieldX`,[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`m14.5 9.5-5 5`,key:`17q4r4`}],[`path`,{d:`m9.5 9.5 5 5`,key:`18nt4w`}]]),ie=P(`Sparkles`,[[`path`,{d:`M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z`,key:`4pj2yx`}],[`path`,{d:`M20 3v4`,key:`1olli1`}],[`path`,{d:`M22 5h-4`,key:`1gvqau`}],[`path`,{d:`M4 17v2`,key:`vumght`}],[`path`,{d:`M5 18H3`,key:`zchphs`}]]),ae=P(`TriangleAlert`,[[`path`,{d:`m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3`,key:`wmoenq`}],[`path`,{d:`M12 9v4`,key:`juzpu7`}],[`path`,{d:`M12 17h.01`,key:`p32p05`}]]),oe=P(`UserRound`,[[`circle`,{cx:`12`,cy:`8`,r:`5`,key:`1hypcn`}],[`path`,{d:`M20 21a8 8 0 0 0-16 0`,key:`rfgkzh`}]]),se=P(`UsersRound`,[[`path`,{d:`M18 21a8 8 0 0 0-16 0`,key:`3ypg7q`}],[`circle`,{cx:`10`,cy:`8`,r:`5`,key:`o932ke`}],[`path`,{d:`M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3`,key:`10s06x`}]]),ce=P(`Activity`,[[`path`,{d:`M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2`,key:`169zse`}]]),le=P(`ArrowLeftRight`,[[`path`,{d:`M8 3 4 7l4 4`,key:`9rb6wj`}],[`path`,{d:`M4 7h16`,key:`6tx8e3`}],[`path`,{d:`m16 21 4-4-4-4`,key:`siv7j2`}],[`path`,{d:`M20 17H4`,key:`h6l3hr`}]]),ue=P(`ArrowLeft`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]),de=P(`AtSign`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8`,key:`7n84p3`}]]),fe=P(`Ban`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m4.9 4.9 14.2 14.2`,key:`1m5liu`}]]),L=P(`Bot`,[[`path`,{d:`M12 8V4H8`,key:`hb8ula`}],[`rect`,{width:`16`,height:`12`,x:`4`,y:`8`,rx:`2`,key:`enze0r`}],[`path`,{d:`M2 14h2`,key:`vft8re`}],[`path`,{d:`M20 14h2`,key:`4cs60a`}],[`path`,{d:`M15 13v2`,key:`1xurst`}],[`path`,{d:`M9 13v2`,key:`rq6x2g`}]]),pe=P(`Building2`,[[`path`,{d:`M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z`,key:`1b4qmf`}],[`path`,{d:`M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2`,key:`i71pzd`}],[`path`,{d:`M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2`,key:`10jefs`}],[`path`,{d:`M10 6h4`,key:`1itunk`}],[`path`,{d:`M10 10h4`,key:`tcdvrf`}],[`path`,{d:`M10 14h4`,key:`kelpxr`}],[`path`,{d:`M10 18h4`,key:`1ulq68`}]]),me=P(`Cable`,[[`path`,{d:`M17 21v-2a1 1 0 0 1-1-1v-1a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1`,key:`10bnsj`}],[`path`,{d:`M19 15V6.5a1 1 0 0 0-7 0v11a1 1 0 0 1-7 0V9`,key:`1eqmu1`}],[`path`,{d:`M21 21v-2h-4`,key:`14zm7j`}],[`path`,{d:`M3 5h4V3`,key:`z442eg`}],[`path`,{d:`M7 5a1 1 0 0 1 1 1v1a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a1 1 0 0 1 1-1V3`,key:`ebdjd7`}]]),he=P(`Check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),ge=P(`ChevronDown`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),_e=P(`ChevronLeft`,[[`path`,{d:`m15 18-6-6 6-6`,key:`1wnfg3`}]]),ve=P(`ChevronRight`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),ye=P(`Copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]),be=P(`Cpu`,[[`rect`,{width:`16`,height:`16`,x:`4`,y:`4`,rx:`2`,key:`14l7u7`}],[`rect`,{width:`6`,height:`6`,x:`9`,y:`9`,rx:`1`,key:`5aljv4`}],[`path`,{d:`M15 2v2`,key:`13l42r`}],[`path`,{d:`M15 20v2`,key:`15mkzm`}],[`path`,{d:`M2 15h2`,key:`1gxd5l`}],[`path`,{d:`M2 9h2`,key:`1bbxkp`}],[`path`,{d:`M20 15h2`,key:`19e6y8`}],[`path`,{d:`M20 9h2`,key:`19tzq7`}],[`path`,{d:`M9 2v2`,key:`165o2o`}],[`path`,{d:`M9 20v2`,key:`i2bqo8`}]]),xe=P(`Database`,[[`ellipse`,{cx:`12`,cy:`5`,rx:`9`,ry:`3`,key:`msslwz`}],[`path`,{d:`M3 5V19A9 3 0 0 0 21 19V5`,key:`1wlel7`}],[`path`,{d:`M3 12A9 3 0 0 0 21 12`,key:`mv7ke4`}]]),Se=P(`ExternalLink`,[[`path`,{d:`M15 3h6v6`,key:`1q9fwt`}],[`path`,{d:`M10 14 21 3`,key:`gplh6r`}],[`path`,{d:`M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6`,key:`a6xqqp`}]]),Ce=P(`Eye`,[[`path`,{d:`M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0`,key:`1nclc0`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]]),R=P(`FileJson`,[[`path`,{d:`M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z`,key:`1rqfz7`}],[`path`,{d:`M14 2v4a2 2 0 0 0 2 2h4`,key:`tnqrlb`}],[`path`,{d:`M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1`,key:`1oajmo`}],[`path`,{d:`M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1`,key:`mpwhp6`}]]),we=P(`Film`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`path`,{d:`M7 3v18`,key:`bbkbws`}],[`path`,{d:`M3 7.5h4`,key:`zfgn84`}],[`path`,{d:`M3 12h18`,key:`1i2n21`}],[`path`,{d:`M3 16.5h4`,key:`1230mu`}],[`path`,{d:`M17 3v18`,key:`in4fa5`}],[`path`,{d:`M17 7.5h4`,key:`myr1c1`}],[`path`,{d:`M17 16.5h4`,key:`go4c1d`}]]),Te=P(`Flag`,[[`path`,{d:`M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z`,key:`i9b6wo`}],[`line`,{x1:`4`,x2:`4`,y1:`22`,y2:`15`,key:`1cm3nv`}]]),Ee=P(`Flame`,[[`path`,{d:`M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z`,key:`96xj49`}]]),De=P(`Handshake`,[[`path`,{d:`m11 17 2 2a1 1 0 1 0 3-3`,key:`efffak`}],[`path`,{d:`m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4`,key:`9pr0kb`}],[`path`,{d:`m21 3 1 11h-2`,key:`1tisrp`}],[`path`,{d:`M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3`,key:`1uvwmv`}],[`path`,{d:`M3 4h8`,key:`1ep09j`}]]),Oe=P(`HardDrive`,[[`line`,{x1:`22`,x2:`2`,y1:`12`,y2:`12`,key:`1y58io`}],[`path`,{d:`M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z`,key:`oot6mr`}],[`line`,{x1:`6`,x2:`6.01`,y1:`16`,y2:`16`,key:`sgf278`}],[`line`,{x1:`10`,x2:`10.01`,y1:`16`,y2:`16`,key:`1l4acy`}]]),ke=P(`History`,[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`,key:`1357e3`}],[`path`,{d:`M3 3v5h5`,key:`1xhq8a`}],[`path`,{d:`M12 7v5l4 2`,key:`1fdv2h`}]]),Ae=P(`ImageOff`,[[`line`,{x1:`2`,x2:`22`,y1:`2`,y2:`22`,key:`a6p6uj`}],[`path`,{d:`M10.41 10.41a2 2 0 1 1-2.83-2.83`,key:`1bzlo9`}],[`line`,{x1:`13.5`,x2:`6`,y1:`13.5`,y2:`21`,key:`1q0aeu`}],[`line`,{x1:`18`,x2:`21`,y1:`12`,y2:`15`,key:`5mozeu`}],[`path`,{d:`M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59`,key:`mmje98`}],[`path`,{d:`M21 15V5a2 2 0 0 0-2-2H9`,key:`43el77`}]]),je=P(`ImagePlus`,[[`path`,{d:`M16 5h6`,key:`1vod17`}],[`path`,{d:`M19 2v6`,key:`4bpg5p`}],[`path`,{d:`M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5`,key:`1ue2ih`}],[`path`,{d:`m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21`,key:`1xmnt7`}],[`circle`,{cx:`9`,cy:`9`,r:`2`,key:`af1f0g`}]]),Me=P(`LayoutDashboard`,[[`rect`,{width:`7`,height:`9`,x:`3`,y:`3`,rx:`1`,key:`10lvy0`}],[`rect`,{width:`7`,height:`5`,x:`14`,y:`3`,rx:`1`,key:`16une8`}],[`rect`,{width:`7`,height:`9`,x:`14`,y:`12`,rx:`1`,key:`1hutg5`}],[`rect`,{width:`7`,height:`5`,x:`3`,y:`16`,rx:`1`,key:`ldoo1y`}]]),Ne=P(`LifeBuoy`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m4.93 4.93 4.24 4.24`,key:`1ymg45`}],[`path`,{d:`m14.83 9.17 4.24-4.24`,key:`1cb5xl`}],[`path`,{d:`m14.83 14.83 4.24 4.24`,key:`q42g0n`}],[`path`,{d:`m9.17 14.83-4.24 4.24`,key:`bqpfvv`}],[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}]]),Pe=P(`LogOut`,[[`path`,{d:`M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4`,key:`1uf3rs`}],[`polyline`,{points:`16 17 21 12 16 7`,key:`1gabdz`}],[`line`,{x1:`21`,x2:`9`,y1:`12`,y2:`12`,key:`1uyos4`}]]),Fe=P(`Mail`,[[`rect`,{width:`20`,height:`16`,x:`2`,y:`4`,rx:`2`,key:`18n3k1`}],[`path`,{d:`m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7`,key:`1ocrg3`}]]),Ie=P(`Megaphone`,[[`path`,{d:`m3 11 18-5v12L3 14v-3z`,key:`n962bs`}],[`path`,{d:`M11.6 16.8a3 3 0 1 1-5.8-1.6`,key:`1yl0tm`}]]),Le=P(`MemoryStick`,[[`path`,{d:`M6 19v-3`,key:`1nvgqn`}],[`path`,{d:`M10 19v-3`,key:`iu8nkm`}],[`path`,{d:`M14 19v-3`,key:`kcehxu`}],[`path`,{d:`M18 19v-3`,key:`1vh91z`}],[`path`,{d:`M8 11V9`,key:`63erz4`}],[`path`,{d:`M16 11V9`,key:`fru6f3`}],[`path`,{d:`M12 11V9`,key:`ha00sb`}],[`path`,{d:`M2 15h20`,key:`16ne18`}],[`path`,{d:`M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.1a2 2 0 0 0 0 3.837V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.1a2 2 0 0 0 0-3.837Z`,key:`lhddv3`}]]),Re=P(`MessageSquareText`,[[`path`,{d:`M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z`,key:`1lielz`}],[`path`,{d:`M13 8H7`,key:`14i4kc`}],[`path`,{d:`M17 12H7`,key:`16if0g`}]]),ze=P(`MonitorSmartphone`,[[`path`,{d:`M18 8V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8`,key:`10dyio`}],[`path`,{d:`M10 19v-3.96 3.15`,key:`1irgej`}],[`path`,{d:`M7 19h5`,key:`qswx4l`}],[`rect`,{width:`6`,height:`10`,x:`16`,y:`12`,rx:`2`,key:`1egngj`}]]),Be=P(`Moon`,[[`path`,{d:`M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z`,key:`a7tn18`}]]),Ve=P(`Palette`,[[`circle`,{cx:`13.5`,cy:`6.5`,r:`.5`,fill:`currentColor`,key:`1okk4w`}],[`circle`,{cx:`17.5`,cy:`10.5`,r:`.5`,fill:`currentColor`,key:`f64h9f`}],[`circle`,{cx:`8.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`fotxhn`}],[`circle`,{cx:`6.5`,cy:`12.5`,r:`.5`,fill:`currentColor`,key:`qy21gx`}],[`path`,{d:`M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z`,key:`12rzf8`}]]),He=P(`Phone`,[[`path`,{d:`M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z`,key:`foiqr5`}]]),Ue=P(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),We=P(`Plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),Ge=P(`PowerOff`,[[`path`,{d:`M18.36 6.64A9 9 0 0 1 20.77 15`,key:`dxknvb`}],[`path`,{d:`M6.16 6.16a9 9 0 1 0 12.68 12.68`,key:`1x7qb5`}],[`path`,{d:`M12 2v4`,key:`3427ic`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}]]),z=P(`Power`,[[`path`,{d:`M12 2v10`,key:`mnfbl`}],[`path`,{d:`M18.4 6.6a9 9 0 1 1-12.77.04`,key:`obofu9`}]]),Ke=P(`Radio`,[[`path`,{d:`M4.9 19.1C1 15.2 1 8.8 4.9 4.9`,key:`1vaf9d`}],[`path`,{d:`M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5`,key:`u1ii0m`}],[`circle`,{cx:`12`,cy:`12`,r:`2`,key:`1c9p78`}],[`path`,{d:`M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5`,key:`1j5fej`}],[`path`,{d:`M19.1 4.9C23 8.8 23 15.1 19.1 19`,key:`10b0cb`}]]),qe=P(`RefreshCw`,[[`path`,{d:`M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8`,key:`v9h5vc`}],[`path`,{d:`M21 3v5h-5`,key:`1q7to0`}],[`path`,{d:`M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16`,key:`3uifl3`}],[`path`,{d:`M8 16H3v5`,key:`1cv678`}]]),Je=P(`ScrollText`,[[`path`,{d:`M15 12h-5`,key:`r7krc0`}],[`path`,{d:`M15 8h-5`,key:`1khuty`}],[`path`,{d:`M19 17V5a2 2 0 0 0-2-2H4`,key:`zz82l3`}],[`path`,{d:`M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3`,key:`1ph1d7`}]]),B=P(`Search`,[[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}],[`path`,{d:`m21 21-4.3-4.3`,key:`1qie3q`}]]),Ye=P(`Send`,[[`path`,{d:`M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z`,key:`1ffxy3`}],[`path`,{d:`m21.854 2.147-10.94 10.939`,key:`12cjpa`}]]),Xe=P(`Settings2`,[[`path`,{d:`M20 7h-9`,key:`3s1dr2`}],[`path`,{d:`M14 17H5`,key:`gfn3mx`}],[`circle`,{cx:`17`,cy:`17`,r:`3`,key:`18b49y`}],[`circle`,{cx:`7`,cy:`7`,r:`3`,key:`dfmy0x`}]]),Ze=P(`ShieldAlert`,[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`M12 8v4`,key:`1got3b`}],[`path`,{d:`M12 16h.01`,key:`1drbdi`}]]),Qe=P(`ShieldCheck`,[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),$e=P(`ShieldOff`,[[`path`,{d:`m2 2 20 20`,key:`1ooewy`}],[`path`,{d:`M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71`,key:`1jlk70`}],[`path`,{d:`M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264`,key:`18rp1v`}]]),V=P(`Smartphone`,[[`rect`,{width:`14`,height:`20`,x:`5`,y:`2`,rx:`2`,ry:`2`,key:`1yt0o3`}],[`path`,{d:`M12 18h.01`,key:`mhygvu`}]]),et=P(`Smile`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M8 14s1.5 2 4 2 4-2 4-2`,key:`1y1vjs`}],[`line`,{x1:`9`,x2:`9.01`,y1:`9`,y2:`9`,key:`yxxnd0`}],[`line`,{x1:`15`,x2:`15.01`,y1:`9`,y2:`9`,key:`1p4y9e`}]]),tt=P(`Stamp`,[[`path`,{d:`M5 22h14`,key:`ehvnwv`}],[`path`,{d:`M19.27 13.73A2.5 2.5 0 0 0 17.5 13h-11A2.5 2.5 0 0 0 4 15.5V17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-1.5c0-.66-.26-1.3-.73-1.77Z`,key:`1sy9ra`}],[`path`,{d:`M14 13V8.5C14 7 15 7 15 5a3 3 0 0 0-3-3c-1.66 0-3 1-3 3s1 2 1 3.5V13`,key:`cnxgux`}]]),nt=P(`Sticker`,[[`path`,{d:`M15.5 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V8.5L15.5 3Z`,key:`1wis1t`}],[`path`,{d:`M14 3v4a2 2 0 0 0 2 2h4`,key:`36rjfy`}],[`path`,{d:`M8 13h.01`,key:`1sbv64`}],[`path`,{d:`M16 13h.01`,key:`wip0gl`}],[`path`,{d:`M10 16s.8 1 2 1c1.3 0 2-1 2-1`,key:`1vvgv3`}]]),rt=P(`Sun`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`m4.93 4.93 1.41 1.41`,key:`149t6j`}],[`path`,{d:`m17.66 17.66 1.41 1.41`,key:`ptbguv`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`m6.34 17.66-1.41 1.41`,key:`1m8zz5`}],[`path`,{d:`m19.07 4.93-1.41 1.41`,key:`1shlcs`}]]),it=P(`Trash2`,[[`path`,{d:`M3 6h18`,key:`d0wm0j`}],[`path`,{d:`M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6`,key:`4alrt4`}],[`path`,{d:`M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2`,key:`v07s0e`}],[`line`,{x1:`10`,x2:`10`,y1:`11`,y2:`17`,key:`1uufr5`}],[`line`,{x1:`14`,x2:`14`,y1:`11`,y2:`17`,key:`xtxkd`}]]),at=P(`Undo2`,[[`path`,{d:`M9 14 4 9l5-5`,key:`102s5s`}],[`path`,{d:`M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11`,key:`f3b9sd`}]]),ot=P(`Upload`,[[`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4`,key:`ih7n3h`}],[`polyline`,{points:`17 8 12 3 7 8`,key:`t8dd8p`}],[`line`,{x1:`12`,x2:`12`,y1:`3`,y2:`15`,key:`widbto`}]]),st=P(`User`,[[`path`,{d:`M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2`,key:`975kel`}],[`circle`,{cx:`12`,cy:`7`,r:`4`,key:`17ys0d`}]]),ct=P(`Users`,[[`path`,{d:`M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2`,key:`1yyitq`}],[`circle`,{cx:`9`,cy:`7`,r:`4`,key:`nufk8`}],[`path`,{d:`M22 21v-2a4 4 0 0 0-3-3.87`,key:`kshegd`}],[`path`,{d:`M16 3.13a4 4 0 0 1 0 7.75`,key:`1da9ce`}]]),lt=P(`Vault`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`circle`,{cx:`7.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`kqv944`}],[`path`,{d:`m7.9 7.9 2.7 2.7`,key:`hpeyl3`}],[`circle`,{cx:`16.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`w0ekpg`}],[`path`,{d:`m13.4 10.6 2.7-2.7`,key:`264c1n`}],[`circle`,{cx:`7.5`,cy:`16.5`,r:`.5`,fill:`currentColor`,key:`nkw3mc`}],[`path`,{d:`m7.9 16.1 2.7-2.7`,key:`p81g5e`}],[`circle`,{cx:`16.5`,cy:`16.5`,r:`.5`,fill:`currentColor`,key:`fubopw`}],[`path`,{d:`m13.4 13.4 2.7 2.7`,key:`abhel3`}],[`circle`,{cx:`12`,cy:`12`,r:`2`,key:`1c9p78`}]]),ut=P(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]);function dt(e){let t=e.trim();return!t||t.startsWith(`+`)?t:/^\d+$/.test(t)?`+${t}`:t}function H(e){let t=e.trim();return t?t.startsWith(`@`)?t:`@${t}`:``}function ft(e){return`${e.FirstName||``} ${e.LastName||``}`.trim()||`-`}function pt(e){return e.Broadcast&&!e.Megagroup?`Channel`:e.Megagroup&&e.Forum?`Supergroup / Forum`:e.Megagroup?`Supergroup`:`Channel / Group`}function U(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function mt(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function ht(e){let t=(e??``).trim();if(!/^https?:\/\//i.test(t))return``;try{let e=new URL(t);return e.protocol!==`http:`&&e.protocol!==`https:`?``:e.href}catch{return``}}function gt(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function _t(e){let t=(e??``).trim();if(!t)return`0`;let n=Number(t);return Number.isFinite(n)?n.toLocaleString():t}var vt={XTR:0,TON:9,USD:2,EUR:2,RUB:2};function yt(e){let t=(e??``).trim().toUpperCase();return t in vt?vt[t]:2}function bt(e,t){let n=(e??``).trim();if(!n)return`0`;if(!/^-?\d+$/.test(n))return n;let r=yt(t),i=n.startsWith(`-`),a=(i?n.slice(1):n).replace(/^0+(?=\d)/,``).padStart(r+1,`0`),o=a.slice(0,a.length-r)||`0`,s=r>0?a.slice(a.length-r):``;r>2&&(s=s.replace(/0+$/,``));let c=i?`-`:``;return s?`${c}${xt(o)}.${s}`:`${c}${xt(o)}`}function xt(e){return e.replace(/\B(?=(\d{3})+(?!\d))/g,` `)}function St(e,t){let n=(t??``).trim().toUpperCase(),r=bt(e,n);return n?`${r} ${n}`:r}function Ct(e,t){let n=(e??``).trim().replace(/\s+/g,``).replace(`,`,`.`);if(!n)return`0`;if(!/^\d*(\.\d*)?$/.test(n)||n===`.`)return null;let r=yt(t),[i,a=``]=n.split(`.`);if(a.length>r)return null;let o=`${i||`0`}${a.padEnd(r,`0`)}`.replace(/^0+(?=\d)/,``);return o===``?`0`:o}function wt(e){let t=(e??``).trim();if(!t||!/^\d+$/.test(t))return`0 B`;let n=Number(t);if(!Number.isFinite(n))return`${t} B`;let r=[`B`,`KB`,`MB`,`GB`,`TB`,`PB`],i=n,a=0;for(;i>=1024&&ae.trim()).filter(Boolean).map(e=>Number.parseInt(e,10));if(n.length===0||n.some(e=>!Number.isFinite(e)||e<=0))throw Error(t);return n}var Et=o((e=>{var t=u(),n=Symbol.for(`react.element`),r=Symbol.for(`react.fragment`),i=Object.prototype.hasOwnProperty,a=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,o={key:!0,ref:!0,__self:!0,__source:!0};function s(e,t,r){var s,c={},l=null,u=null;for(s in r!==void 0&&(l=``+r),t.key!==void 0&&(l=``+t.key),t.ref!==void 0&&(u=t.ref),t)i.call(t,s)&&!o.hasOwnProperty(s)&&(c[s]=t[s]);if(e&&e.defaultProps)for(s in t=e.defaultProps,t)c[s]===void 0&&(c[s]=t[s]);return{$$typeof:n,type:e,key:l,ref:u,props:c,_owner:a.current}}e.Fragment=r,e.jsx=s,e.jsxs=s})),W=o(((e,t)=>{t.exports=Et()}))();function Dt({title:e,eyebrow:t,children:n,actions:r}){return(0,W.jsxs)(`div`,{className:`page-frame`,children:[(0,W.jsxs)(`div`,{className:`page-title-row`,children:[(0,W.jsxs)(`div`,{children:[t&&(0,W.jsx)(`div`,{className:`eyebrow`,children:t}),(0,W.jsx)(`h2`,{children:e})]}),r&&(0,W.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Ot({children:e}){return(0,W.jsx)(`div`,{className:`query-panel`,children:e})}function kt({main:e,side:t}){return(0,W.jsxs)(`div`,{className:`split-layout`,children:[(0,W.jsx)(`div`,{className:`split-main`,children:e}),(0,W.jsx)(`aside`,{className:`split-side`,children:t})]})}function G({title:e,text:t,action:n}){return(0,W.jsxs)(`div`,{className:`section-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`h2`,{children:e}),t&&(0,W.jsx)(`p`,{children:t})]}),n&&(0,W.jsx)(`div`,{className:`section-action`,children:n})]})}function K({children:e}){return(0,W.jsxs)(`div`,{className:`alert`,children:[(0,W.jsx)(te,{size:16}),` `,(0,W.jsx)(`span`,{children:e})]})}function q({children:e,tone:t=`neutral`}){return(0,W.jsx)(`span`,{className:`badge ${t}`,children:e})}function J({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,W.jsxs)(`div`,{className:`metric ${n}`,children:[(0,W.jsx)(`span`,{children:e}),(0,W.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function Y({label:e,value:t,mono:n=!1}){return(0,W.jsxs)(`div`,{className:`summary-item`,children:[(0,W.jsx)(`span`,{children:e}),(0,W.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function At({rows:e}){return(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Command ID`}),(0,W.jsx)(`th`,{children:`Action`}),(0,W.jsx)(`th`,{children:`Actor`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Dry-run`}),(0,W.jsx)(`th`,{children:`Reason`}),(0,W.jsx)(`th`,{children:`Time`})]})}),(0,W.jsxs)(`tbody`,{children:[e.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.ID}),(0,W.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,W.jsx)(`td`,{children:e.Action}),(0,W.jsx)(`td`,{children:e.Actor}),(0,W.jsx)(`td`,{children:e.Status}),(0,W.jsx)(`td`,{children:e.DryRun?`Yes`:`No`}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,W.jsx)(`td`,{children:U(e.CreatedAt)})]},e.ID)),e.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})})}function jt({colSpan:e}){return(0,W.jsx)(`tr`,{children:(0,W.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:`No results`})})}function X({label:e}){return(0,W.jsx)(`section`,{className:`surface`,children:(0,W.jsx)(`div`,{className:`loading-line`,children:e})})}function Mt({value:e}){return(0,W.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function Nt({username:e,collectibles:t}){let n=H(e??``),r=t??[];return r.length===0?(0,W.jsx)(W.Fragment,{children:n||`-`}):(0,W.jsxs)(W.Fragment,{children:[n,(0,W.jsx)(`ul`,{className:`username-branch`,children:r.map(e=>(0,W.jsxs)(`li`,{className:e.Active?``:`inactive`,children:[(0,W.jsx)(`span`,{children:H(e.Username)}),!e.Active&&(0,W.jsx)(`em`,{children:`inactive`})]},e.Username))})]})}var Pt=`verification.review`,Ft=`botverification.review`,It=`botverification.manage`,Lt=(0,g.createContext)({permissions:[],hideThirdPartyVerification:!0});function Rt({permissions:e,hideThirdPartyVerification:t=!0,children:n}){let r=(0,g.useMemo)(()=>({permissions:e,hideThirdPartyVerification:t}),[e,t]);return(0,W.jsx)(Lt.Provider,{value:r,children:n})}function zt(){let{permissions:e}=(0,g.useContext)(Lt);return(0,g.useMemo)(()=>({permissions:e,can:t=>e.includes(`*`)||e.includes(t)}),[e])}function Bt(e){return zt().can(e)}function Vt(){return(0,g.useContext)(Lt).hideThirdPartyVerification}function Ht({permission:e,children:t}){let{can:n}=zt();return n(e)?(0,W.jsx)(W.Fragment,{children:t}):(0,W.jsx)(Ut,{permission:e})}function Ut({permission:e}){return(0,W.jsxs)(Dt,{title:`Not enough rights`,eyebrow:`Console / Access`,children:[(0,W.jsx)(K,{children:`This session was not granted the ${e} permission, so the section stays closed.`}),(0,W.jsx)(`section`,{className:`section-block`,children:(0,W.jsx)(`div`,{className:`entity-head`,children:(0,W.jsxs)(`div`,{children:[(0,W.jsxs)(`div`,{className:`entity-title`,children:[(0,W.jsx)($e,{size:16}),` `,`Section unavailable`]}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`Ask an operator to add the permission to TELESRV_ADMIN_UI_PERMISSIONS and sign in again.`})]})})})]})}function Wt({children:e}){return Vt()?(0,W.jsxs)(Dt,{title:`Feature hidden`,eyebrow:`Console / Third-party marks`,children:[(0,W.jsx)(K,{children:`Third-party bot verification is hidden on this server (TELESRV_HIDE_THIRD_PARTY_VERIFICATION=true).`}),(0,W.jsx)(`section`,{className:`section-block`,children:(0,W.jsx)(`div`,{className:`entity-head`,children:(0,W.jsxs)(`div`,{children:[(0,W.jsxs)(`div`,{className:`entity-title`,children:[(0,W.jsx)($e,{size:16}),` `,`Not fully finished`]}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`This feature may cause unstable server behavior and is hidden by default. Set TELESRV_HIDE_THIRD_PARTY_VERIFICATION=false to re-enable it.`})]})})})]}):(0,W.jsx)(W.Fragment,{children:e})}function Gt(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function Kt(e){return e.startsWith(`/bot-verification`)?`Third-party verification`:e.startsWith(`/verification`)?`Official Verification`:e.startsWith(`/collectible-usernames`)?`Collectible Usernames`:e.startsWith(`/storage`)?`Storage`:e.startsWith(`/accounts/shared-devices`)?`Shared Devices`:e.startsWith(`/accounts`)?`Accounts`:e.startsWith(`/channels`)?`Supergroups and Channels`:e.startsWith(`/bots`)?`Bots`:e.startsWith(`/moderation`)?`Reports and Moderation`:e.startsWith(`/broadcasts`)?`Broadcasts`:e.startsWith(`/emoji`)?`Emoji`:e.startsWith(`/messages`)?`Message Audit`:e.startsWith(`/stickers`)?`Stickers`:e.startsWith(`/gif-catalog`)?`GIFs`:`Operations Console`}var qt=`telesrv.admin.theme`,Jt=(0,g.createContext)(null);function Yt(e){document.documentElement.setAttribute(`data-theme`,e),document.documentElement.style.colorScheme=e}function Xt({children:e}){let[t,n]=(0,g.useState)(()=>$t());(0,g.useEffect)(()=>{Yt(t);try{localStorage.setItem(qt,t)}catch{}},[t]),(0,g.useEffect)(()=>{if(!window.matchMedia)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`),t=e=>{let t=null;try{t=localStorage.getItem(qt)}catch{t=null}t!==`light`&&t!==`dark`&&n(e.matches?`dark`:`light`)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]);let r=(0,g.useCallback)(e=>n(e),[]),i=(0,g.useCallback)(()=>n(e=>e===`dark`?`light`:`dark`),[]),a=(0,g.useMemo)(()=>({theme:t,setTheme:r,toggleTheme:i}),[t,r,i]);return(0,W.jsx)(Jt.Provider,{value:a,children:e})}function Zt(){let e=(0,g.useContext)(Jt);if(!e)throw Error(`useTheme must be used inside ThemeProvider`);return e}function Qt(){let{theme:e,toggleTheme:t}=Zt(),n=e===`light`?`Switch to dark theme`:`Switch to light theme`;return(0,W.jsx)(`button`,{className:`theme-toggle`,type:`button`,onClick:t,"aria-label":n,title:n,children:e===`dark`?(0,W.jsx)(rt,{size:16}):(0,W.jsx)(Be,{size:16})})}function $t(){try{let e=localStorage.getItem(qt);if(e===`light`||e===`dark`)return e}catch{}try{if(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches)return`dark`}catch{}return`light`}function en({href:e,navigate:t,className:n,children:r}){return(0,W.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function tn(){return(0,W.jsxs)(`div`,{className:`boot-screen`,children:[(0,W.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:(0,W.jsx)(`img`,{src:`/logo.png`,alt:`OwpenGram`})}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`OwpenGram`}),(0,W.jsx)(`small`,{children:`Admin Console`})]})]}),(0,W.jsx)(`div`,{className:`loader-bar`})]})}function nn({actor:e,route:t,navigate:n,onLogout:r,children:i}){let a=Bt(Pt),o=Bt(Ft),s=Vt(),c=t.path.startsWith(`/messages`),[l,u]=(0,g.useState)(c);(0,g.useEffect)(()=>{c&&u(!0)},[c]);async function d(){await k.logout().catch(()=>void 0),r()}return(0,W.jsxs)(`div`,{className:`shell`,children:[(0,W.jsxs)(`aside`,{className:`sidebar`,children:[(0,W.jsxs)(en,{className:`brand`,href:`/`,navigate:n,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:(0,W.jsx)(`img`,{src:`/logo.png`,alt:`OwpenGram`})}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`OwpenGram`}),(0,W.jsx)(`small`,{children:`Admin Console`})]})]}),(0,W.jsx)(`div`,{className:`sidebar-label`,children:`Navigation`}),(0,W.jsxs)(`nav`,{className:`nav-list`,"aria-label":`Primary navigation`,children:[(0,W.jsx)(rn,{icon:(0,W.jsx)(Me,{size:16}),href:`/`,route:t,navigate:n,children:`Overview`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(ct,{size:16}),href:`/accounts`,route:t,navigate:n,children:`Accounts`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(Qe,{size:16}),href:`/channels`,route:t,navigate:n,children:`Supergroups / Channels`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(L,{size:16}),href:`/bots`,route:t,navigate:n,children:`Bots`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(Ze,{size:16}),href:`/moderation`,route:t,navigate:n,children:`Reports / Moderation`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(Ie,{size:16}),href:`/broadcasts`,route:t,navigate:n,children:`Broadcasts`}),a&&(0,W.jsx)(rn,{icon:(0,W.jsx)(ee,{size:16}),href:`/verification`,route:t,navigate:n,children:`Verification`}),o&&!s&&(0,W.jsx)(rn,{icon:(0,W.jsx)(tt,{size:16}),href:`/bot-verification`,route:t,navigate:n,children:`Third-party marks`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(de,{size:16}),href:`/collectible-usernames`,route:t,navigate:n,children:`NFT Usernames`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(xe,{size:16}),href:`/storage`,route:t,navigate:n,children:`Storage`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(nt,{size:16}),href:`/stickers`,route:t,navigate:n,children:`Stickers`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(et,{size:16}),href:`/emoji`,route:t,navigate:n,children:`Emoji`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(we,{size:16}),href:`/gif-catalog`,route:t,navigate:n,children:`GIFs`}),(0,W.jsxs)(`div`,{className:`nav-section ${c?`active`:``} ${l?`open`:``}`,children:[(0,W.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":l,onClick:()=>u(e=>!e),children:[(0,W.jsx)(Re,{size:16}),(0,W.jsx)(`span`,{children:`Messages`}),(0,W.jsx)(ge,{className:`nav-section-chevron`,size:15})]}),l&&(0,W.jsxs)(`div`,{className:`nav-children`,children:[(0,W.jsx)(rn,{href:`/messages/private`,route:t,navigate:n,activeWhen:e=>e===`/messages`||e===`/messages/detail`||e.startsWith(`/messages/private`),children:`Private`}),(0,W.jsx)(rn,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:`Groups`})]})]})]})]}),(0,W.jsxs)(`div`,{className:`workspace`,children:[(0,W.jsxs)(`header`,{className:`topbar`,children:[(0,W.jsx)(`div`,{children:(0,W.jsx)(`h1`,{children:Kt(t.path)})}),(0,W.jsxs)(`div`,{className:`topbar-actions`,children:[(0,W.jsx)(Qt,{}),(0,W.jsx)(`span`,{className:`actor-pill`,children:`Actor: ${e}`}),(0,W.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:d,title:`Log out`,children:[(0,W.jsx)(Pe,{size:16}),` `,`Log out`]})]})]}),(0,W.jsx)(`main`,{className:`content`,children:i})]})]})}function rn({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,W.jsxs)(en,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,W.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,W.jsx)(`span`,{children:i})]})}function an({onLogin:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1);async function s(n){n.preventDefault(),o(!0),i(``);try{let n=await k.login(t);e({actor:n.actor,permissions:n.permissions??[]})}catch(e){i(O(e))}finally{o(!1)}}return(0,W.jsxs)(`main`,{className:`login-page`,children:[(0,W.jsxs)(`div`,{className:`bg-orbs`,"aria-hidden":`true`,children:[(0,W.jsx)(`div`,{className:`bg-orb bg-orb--1`}),(0,W.jsx)(`div`,{className:`bg-orb bg-orb--2`}),(0,W.jsx)(`div`,{className:`bg-orb bg-orb--3`})]}),(0,W.jsxs)(`section`,{className:`login-panel`,children:[(0,W.jsxs)(`div`,{className:`login-head`,children:[(0,W.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:(0,W.jsx)(`img`,{src:`/logo.png`,alt:`OwpenGram`})}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`OwpenGram`}),(0,W.jsx)(`small`,{children:`Admin Console`})]})]}),(0,W.jsxs)(`div`,{className:`login-head-actions`,children:[(0,W.jsx)(Qt,{}),(0,W.jsx)(`span`,{className:`login-chip`,children:`Local access`})]})]}),(0,W.jsxs)(`div`,{className:`login-copy`,children:[(0,W.jsx)(`h1`,{children:`Operations Admin`}),(0,W.jsx)(`p`,{children:`Enter credentials to open the console.`})]}),r&&(0,W.jsx)(K,{children:r}),(0,W.jsxs)(`form`,{className:`form-stack`,onSubmit:s,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:`Admin password or token`}),(0,W.jsx)(`input`,{autoFocus:!0,type:`password`,value:t,autoComplete:`current-password`,onChange:e=>n(e.target.value)})]}),(0,W.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:a,children:a?`Logging in`:`Log in`})]})]})]})}var on=m();function sn({kind:e,id:t,onClose:n,onDone:r}){let[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``);(0,g.useEffect)(()=>{if(!i){s(``);return}let e=URL.createObjectURL(i);return s(e),()=>URL.revokeObjectURL(e)},[i]);async function m(){if(!i){p(`Choose an image file first.`);return}if(!c.trim()){p(`Please enter an operation reason`);return}d(!0),p(``);try{let a=e===`channel`?`channel_id`:`user_id`,o=new FormData;o.set(`metadata`,JSON.stringify({command_id:``,reason:c.trim(),confirm:!0,[a]:t})),o.set(`file`,i,i.name);let s=e===`channel`?await k.setChannelAvatar(o):await k.setAccountAvatar(o);if(s.error){p(s.error);return}r(),n()}catch(e){p(O(e))}finally{d(!1)}}return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Change avatar`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:e===`channel`?`Channel`:`Account`}),(0,W.jsx)(`h2`,{children:`Change avatar`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:n,disabled:u,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsxs)(`label`,{className:`gift-file-picker ${i?`has-file`:``}`,children:[(0,W.jsx)(`input`,{type:`file`,accept:`image/png,image/jpeg,image/webp`,onChange:e=>a(e.target.files?.[0]??null)}),o?(0,W.jsx)(`img`,{className:`gift-file-icon`,src:o,alt:``,style:{objectFit:`cover`}}):(0,W.jsx)(je,{size:22}),(0,W.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,W.jsx)(`span`,{className:`gift-field-label`,children:`New avatar`}),(0,W.jsx)(`strong`,{children:i?i.name:`Choose a JPEG, PNG, or WebP image`})]}),(0,W.jsx)(`span`,{className:`gift-file-action`,children:i?`Change file`:`Choose file`})]}),(0,W.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,W.jsx)(`span`,{children:`Audit reason`}),(0,W.jsx)(`input`,{value:c,placeholder:`Briefly describe why this avatar is being changed`,onChange:e=>l(e.target.value)})]}),f&&(0,W.jsx)(K,{children:f})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:n,disabled:u,children:`Close`}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:m,disabled:u,children:[u?(0,W.jsx)(I,{className:`spin`,size:15}):(0,W.jsx)(ot,{size:15}),`Upload avatar`]})]})]})}),document.body)}function Z({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,disabled:o=!1,onDone:s,onError:c,secretField:l}){let[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(null),[_,v]=(0,g.useState)(``),[y,b]=(0,g.useState)(!1),[x,S]=(0,g.useState)(!1);function C(){p(``),h(null),v(``),S(!1)}async function w(e){if(!f.trim()){v(`Please enter an operation reason`);return}b(!0),v(``);try{let r={...n(),reason:f,confirm:e};h(await k.action(t,r)),e&&s?.()}catch(e){v(c?.(e)||O(e))}finally{b(!1)}}let T=m?.dry_run&&!m.error,E=`btn ${a===`danger`?`danger`:a===`warn`?`warn`:``} ${i?`compact-btn`:``}`,D=(0,g.useMemo)(()=>{try{return n()}catch(e){return{payload_error:O(e)}}},[u,n]),A=l&&m?.details&&typeof m.details[l]==`string`?m.details[l]:``,j=A&&m?.details?Object.fromEntries(Object.entries(m.details).filter(([e])=>e!==l)):m?.details;async function M(){await navigator.clipboard.writeText(A),S(!0)}return(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:E,type:`button`,disabled:o,onClick:()=>{C(),d(!0)},children:[r,e]}),u&&(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`Action Flow`}),(0,W.jsx)(`h2`,{children:e})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>d(!1),"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsxs)(`div`,{className:`command-steps`,children:[(0,W.jsxs)(`div`,{className:`command-step ${f.trim()?`done`:`active`}`,children:[(0,W.jsx)(`span`,{children:`1`}),(0,W.jsx)(`strong`,{children:`Enter reason`})]}),(0,W.jsxs)(`div`,{className:`command-step ${m?.dry_run?`done`:f.trim()?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`2`}),(0,W.jsx)(`strong`,{children:`Dry-run check`})]}),(0,W.jsxs)(`div`,{className:`command-step ${m&&!m.dry_run&&!m.error?`done`:T?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`3`}),(0,W.jsx)(`strong`,{children:`Confirm execution`})]})]}),(0,W.jsxs)(`label`,{className:`form-field`,children:[(0,W.jsx)(`span`,{children:`Operation reason`}),(0,W.jsx)(`textarea`,{value:f,onChange:e=>p(e.target.value),rows:3,placeholder:`Describe why this operation is being performed`})]}),(0,W.jsxs)(`div`,{className:`command-preview`,children:[(0,W.jsxs)(`div`,{className:`preview-head`,children:[(0,W.jsx)(R,{size:14}),` `,`Request preview`]}),(0,W.jsx)(Mt,{value:JSON.stringify(D,null,2)})]}),_&&(0,W.jsx)(K,{children:_}),m&&(0,W.jsxs)(`div`,{className:`result-box`,children:[(0,W.jsxs)(`div`,{className:`result-title`,children:[m.error?(0,W.jsx)(te,{size:16}):(0,W.jsx)(F,{size:16}),(0,W.jsx)(`strong`,{children:m.message||m.error||`Action result`})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:`Command ID`}),(0,W.jsx)(`strong`,{children:m.command_id})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:`Status`}),(0,W.jsx)(`strong`,{children:m.status})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:`Dry-run`}),(0,W.jsx)(`strong`,{children:m.dry_run?`Yes`:`No`})]}),(0,W.jsx)(`div`,{className:`result-message`,children:m.message||m.error}),A&&(0,W.jsxs)(`div`,{className:`secret-reveal`,children:[(0,W.jsx)(`div`,{className:`secret-reveal-label`,children:`One-time secret — copy it now, it won't be shown again`}),(0,W.jsxs)(`div`,{className:`secret-reveal-row`,children:[(0,W.jsx)(`code`,{className:`secret-reveal-value`,children:`•`.repeat(Math.min(A.length,40))}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void M(),children:[x?(0,W.jsx)(he,{size:15}):(0,W.jsx)(ye,{size:15}),x?`Copied`:`Copy`]})]})]}),j&&Object.keys(j).length>0&&(0,W.jsx)(Mt,{value:JSON.stringify(j,null,2)})]})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>d(!1),children:`Close`}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>w(!1),disabled:y,children:[y?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(Ue,{size:15}),m?`Run dry-run again`:`Run dry-run first`]}),(0,W.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>w(!0),disabled:y||!T,children:[(0,W.jsx)(F,{size:15}),`Confirm execution`]})]})]})}),document.body)]})}var cn=[[`#FF885E`,`#FF516A`],[`#FFCD6A`,`#FFA85C`],[`#82B1FF`,`#665FFF`],[`#A0DE7E`,`#54CB68`],[`#53EDD6`,`#28C9B7`],[`#72D5FD`,`#2A9EF1`],[`#E0A2F3`,`#D669ED`]];function ln(e){return cn[Math.abs(e)%cn.length]}function un(e){let t=Array.from(e);return t.length>0?t[0]:``}function dn(e,t,n){let r=`${e} ${t}`.trim().split(/\s+/).filter(Boolean),i=r.length>0?r:n?[n]:[];if(i.length===0)return`T`;let a=un(i[0]);return i.length>1&&(a+=un(i[i.length-1])),a.toUpperCase()}function fn({id:e,kind:t=`user`,firstName:n=``,lastName:r=``,username:i=``,title:a=``,size:o=34,refreshKey:s}){let[c,l]=(0,g.useState)(!1);if((0,g.useEffect)(()=>{l(!1)},[e,t,s]),c){let[s,c]=ln(e);return(0,W.jsx)(`div`,{className:`avatar-fallback`,style:{width:o,height:o,background:`linear-gradient(135deg, ${s}, ${c})`,fontSize:Math.round(o*.42)},children:t===`channel`?dn(a,``,i):dn(n,r,i)})}return(0,W.jsx)(`img`,{className:`avatar-photo-img`,src:`${t===`channel`?`/api/channels/${e}/avatar`:`/api/accounts/${e}/avatar`}${s===void 0?``:`?v=${encodeURIComponent(String(s))}`}`,alt:``,loading:`lazy`,style:{width:o,height:o},onError:()=>l(!0)})}function pn({rows:e,userID:t,onDone:n}){let[r,i]=(0,g.useState)(()=>new Set);(0,g.useEffect)(()=>{i(new Set)},[t]);let a=(0,g.useMemo)(()=>e.filter(e=>!r.has(e.Hash)),[e,r]);function o(e){i(t=>e(t)),n()}return(0,W.jsxs)(`div`,{className:`authorization-block`,children:[(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Device`}),(0,W.jsx)(`th`,{children:`Platform`}),(0,W.jsx)(`th`,{children:`IP`}),(0,W.jsx)(`th`,{children:`Last active`}),(0,W.jsx)(`th`,{className:`device-actions-head`,children:`Actions`})]})}),(0,W.jsxs)(`tbody`,{children:[a.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,W.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,W.jsx)(`td`,{children:n.IP}),(0,W.jsx)(`td`,{children:U(n.ActiveAt)}),(0,W.jsx)(`td`,{className:`device-actions-cell`,children:(0,W.jsxs)(`div`,{className:`device-actions`,children:[(0,W.jsx)(Z,{label:`Revoke current`,icon:(0,W.jsx)(Pe,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,hash:n.Hash}),onDone:()=>o(e=>new Set([...e,n.Hash]))}),(0,W.jsx)(Z,{label:`Keep current`,icon:(0,W.jsx)(Qe,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,keep_hash:n.Hash}),onDone:()=>o(()=>new Set(e.filter(e=>e.Hash!==n.Hash).map(e=>e.Hash)))})]})})]},n.Hash)),a.length===0&&(0,W.jsx)(jt,{colSpan:5})]})]})}),(0,W.jsx)(`div`,{className:`danger-zone`,children:(0,W.jsx)(Z,{label:`Revoke all devices`,icon:(0,W.jsx)(me,{size:15}),path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,revoke_all:!0}),onDone:()=>o(()=>new Set(e.map(e=>e.Hash)))})})]})}function mn({scam:e,fake:t}){return!e&&!t?null:(0,W.jsxs)(W.Fragment,{children:[e&&(0,W.jsx)(q,{tone:`danger`,children:`SCAM`}),t&&(0,W.jsx)(q,{tone:`danger`,children:`FAKE`})]})}function hn({idKey:e,id:t,path:n,scam:r,fake:i,onDone:a}){return(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:r?`Clear SCAM`:`Mark as SCAM`,icon:(0,W.jsx)(Ze,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,scam:!r,fake:r?i:!1}),onDone:a}),(0,W.jsx)(Z,{label:i?`Clear FAKE`:`Mark as FAKE`,icon:(0,W.jsx)(re,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,fake:!i,scam:i?r:!1}),onDone:a})]})}function gn({id:e,support:t,onDone:n}){return(0,W.jsx)(Z,{label:t?`Clear support`:`Mark as support`,icon:(0,W.jsx)(Ne,{size:15}),tone:`neutral`,path:`/api/actions/set-support`,payload:()=>({user_id:e,support:!t}),onDone:n})}function _n({idKey:e,id:t,path:n,current:r,onDone:i}){let[a,o]=(0,g.useState)(r.replace(/^@/,``));return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Username`}),(0,W.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`username`})]}),(0,W.jsx)(Z,{label:`Set username`,icon:(0,W.jsx)(de,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,username:a.trim().replace(/^@/,``)}),onDone:i})]})}function vn({id:e,path:t,currentFirstName:n,currentLastName:r,onDone:i}){let[a,o]=(0,g.useState)(n),[s,c]=(0,g.useState)(r);return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`First name`}),(0,W.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`First name`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Last name`}),(0,W.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`Last name`})]}),(0,W.jsx)(Z,{label:`Set name`,icon:(0,W.jsx)(oe,{size:15}),tone:`neutral`,path:t,payload:()=>({user_id:e,first_name:a.trim(),last_name:s.trim()}),onDone:i})]})}function yn({id:e,path:t,current:n,onDone:r}){let[i,a]=(0,g.useState)(n);return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Phone number`}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`15551234567`})]}),(0,W.jsx)(Z,{label:`Set phone`,icon:(0,W.jsx)(He,{size:15}),tone:`warn`,path:t,payload:()=>({user_id:e,phone:i.trim()}),onDone:r})]})}function bn({id:e,path:t,current:n,onDone:r}){let[i,a]=(0,g.useState)(n);return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Login email`}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`name@example.com (empty clears it)`,type:`email`})]}),(0,W.jsx)(Z,{label:i.trim()?`Set login email`:`Clear login email`,icon:(0,W.jsx)(Fe,{size:15}),tone:`warn`,path:t,payload:()=>({user_id:e,email:i.trim()}),onDone:r})]})}function xn({idKey:e,id:t,path:n,onDone:r}){let[i,a]=(0,g.useState)(!1),[o,s]=(0,g.useState)(!0),[c,l]=(0,g.useState)(`0`),[u,d]=(0,g.useState)(``);return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:i,onChange:e=>a(e.target.checked)}),` `,`Profile color`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:o,onChange:e=>s(e.target.checked)}),` `,`Enable color`]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Color index`}),(0,W.jsx)(`input`,{type:`number`,min:`0`,max:`20`,value:c,onChange:e=>l(e.target.value)})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Background emoji ID`}),(0,W.jsx)(`input`,{value:u,onChange:e=>d(e.target.value),placeholder:`0`})]}),(0,W.jsx)(Z,{label:`Set color`,icon:(0,W.jsx)(Ve,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,for_profile:i,has_color:o,color:gt(c),background_emoji_id:u.trim()||`0`}),onDone:r})]})}function Sn({idKey:e,id:t,path:n,onDone:r}){let[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(`0`);return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Emoji document ID`}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`0 = clear`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Until (unix, 0 = permanent)`}),(0,W.jsx)(`input`,{type:`number`,min:`0`,value:o,onChange:e=>s(e.target.value)})]}),(0,W.jsx)(Z,{label:`Set emoji status`,icon:(0,W.jsx)(et,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,document_id:i.trim()||`0`,until:gt(o)}),onDone:r})]})}function Cn({channel:e,onDone:t}){let[n,r]=(0,g.useState)(e.Gigagroup),[i,a]=(0,g.useState)(e.AntiSpam),[o,s]=(0,g.useState)(e.ParticipantsHidden),[c,l]=(0,g.useState)(e.NoForwards),[u,d]=(0,g.useState)(e.JoinToSend),[f,p]=(0,g.useState)(e.JoinRequest),[m,h]=(0,g.useState)(String(e.SlowmodeSeconds));(0,g.useEffect)(()=>{r(e.Gigagroup),a(e.AntiSpam),s(e.ParticipantsHidden),l(e.NoForwards),d(e.JoinToSend),p(e.JoinRequest),h(String(e.SlowmodeSeconds))},[e]);function _(){let t={channel_id:e.ID};return n!==e.Gigagroup&&(t.gigagroup=n),i!==e.AntiSpam&&(t.antispam=i),o!==e.ParticipantsHidden&&(t.participants_hidden=o),c!==e.NoForwards&&(t.noforwards=c),u!==e.JoinToSend&&(t.join_to_send=u),f!==e.JoinRequest&&(t.join_request=f),gt(m)!==e.SlowmodeSeconds&&(t.slowmode_seconds=gt(m)),t}return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:n,onChange:e=>r(e.target.checked)}),` `,`Gigagroup`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:i,onChange:e=>a(e.target.checked)}),` `,`Aggressive anti-spam`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:o,onChange:e=>s(e.target.checked)}),` `,`Hide members`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:c,onChange:e=>l(e.target.checked)}),` `,`Restrict forwarding`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:u,onChange:e=>d(e.target.checked)}),` `,`Join to send messages`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:f,onChange:e=>p(e.target.checked)}),` `,`Join by request`]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Slowmode (seconds)`}),(0,W.jsx)(`input`,{type:`number`,min:`0`,max:`86400`,value:m,onChange:e=>h(e.target.value)})]}),(0,W.jsx)(Z,{label:`Apply settings`,icon:(0,W.jsx)(Xe,{size:15}),tone:`warn`,path:`/api/actions/set-channel-settings`,payload:_,onDone:t})]})}function wn({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(`1`),[f,p]=(0,g.useState)(()=>Tn(new Date(Date.now()+7*864e5))),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(!1),[y,b]=(0,g.useState)(0);async function x(){s(!0),a(``);try{let t=await k.account(e);r(t),t.Restriction.Frozen&&(t.Restriction.Until&&p(Tn(new Date(t.Restriction.Until))),h(t.Restriction.AppealURL||``))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{x(),l(`profile`)},[e]),i)return(0,W.jsx)(K,{children:i});if(!n)return(0,W.jsx)(X,{label:o?`Loading account detail`:`Waiting for data`});let S=n.Account,C=[{key:`profile`,label:`Profile & Status`,icon:(0,W.jsx)(oe,{size:15})},{key:`devices`,label:`Authorized Devices`,icon:(0,W.jsx)(ze,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,W.jsx)(Xe,{size:15})}];return(0,W.jsxs)(Dt,{title:`Account #${S.ID}`,eyebrow:`Account Profile`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,W.jsx)(ue,{size:15}),` `,`Back to list`]}),children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{className:`entity-head-main`,children:[(0,W.jsxs)(`div`,{className:`avatar-edit-slot`,children:[(0,W.jsx)(fn,{id:S.ID,firstName:S.FirstName,lastName:S.LastName,username:S.Username,size:64,refreshKey:y||void 0}),(0,W.jsx)(`button`,{className:`icon-btn avatar-edit-btn`,type:`button`,"aria-label":`Change avatar`,title:`Change avatar`,onClick:()=>v(!0),children:(0,W.jsx)(je,{size:13})})]}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:ft(S)}),(0,W.jsxs)(`div`,{className:`entity-subtitle`,children:[H(S.Username)||`No username`,` · `,dt(S.Phone)||`No phone`]}),S.Collectibles?.length>0&&(0,W.jsx)(`div`,{className:`entity-subtitle`,children:(0,W.jsx)(Nt,{username:``,collectibles:S.Collectibles})})]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[S.PremiumUntil>0?(0,W.jsx)(q,{tone:`good`,children:`Premium`}):(0,W.jsx)(q,{children:`Not premium`}),n.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Not verified`}),(0,W.jsx)(mn,{scam:n.Scam,fake:n.Fake}),S.Frozen?(0,W.jsx)(q,{tone:`danger`,children:`Account frozen`}):(0,W.jsx)(q,{children:`Account active`})]})]}),(0,W.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Account sections`,children:C.map(e=>(0,W.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`User ID`,value:String(S.ID),mono:!0}),(0,W.jsx)(Y,{label:`Last active`,value:mt(n.LastSeenAt)||`-`}),(0,W.jsx)(Y,{label:`Premium expires`,value:S.PremiumUntil>0?mt(S.PremiumUntil):`None`}),(0,W.jsx)(Y,{label:`Updated`,value:U(S.UpdatedAt)||`-`}),(0,W.jsx)(Y,{label:`Authorized devices`,value:String(n.Authorizations.length)}),(0,W.jsx)(Y,{label:`Account flags`,value:`support=${n.Support} bot=${n.Bot}`}),(0,W.jsx)(Y,{label:`Restriction`,value:n.HasRestriction?n.Restriction.Reason||`Restricted`:`None`}),(0,W.jsx)(Y,{label:`Frozen since`,value:n.Restriction.Since?U(n.Restriction.Since):`None`}),(0,W.jsx)(Y,{label:`Appeal deadline`,value:n.Restriction.Until?U(n.Restriction.Until):`None`}),(0,W.jsx)(Y,{label:`Appeal URL`,value:n.Restriction.AppealURL||`None`}),(0,W.jsx)(Y,{label:`Created`,value:U(S.CreatedAt)||`-`})]}),n.About&&(0,W.jsx)(`p`,{className:`about-text`,children:n.About})]}),c===`devices`&&(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Authorized Devices`,text:`${n.Authorizations.length} authorizations`}),(0,W.jsx)(pn,{rows:n.Authorizations,userID:S.ID,onDone:x})]}),c===`actions`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`action-groups`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Freeze & Restriction`,text:`Blocks sign-in and marks the account for appeal review.`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Appeal deadline`}),(0,W.jsx)(`input`,{"aria-label":`Freeze appeal deadline`,value:f,onChange:e=>p(e.target.value),type:`datetime-local`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Appeal URL`}),(0,W.jsx)(`input`,{"aria-label":`Freeze appeal URL`,value:m,onChange:e=>h(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,W.jsx)(Z,{label:S.Frozen?`Update freeze`:`Freeze account`,icon:(0,W.jsx)(te,{size:15}),tone:`danger`,path:`/api/actions/set-frozen`,payload:()=>({user_id:S.ID,frozen:!0,freeze_until:new Date(f).toISOString(),freeze_appeal_url:m.trim()}),onDone:x}),S.Frozen&&(0,W.jsx)(Z,{label:`Unfreeze account`,icon:(0,W.jsx)(te,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:S.ID,frozen:!1}),onDone:x})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Premium`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Premium duration (months)`}),(0,W.jsx)(`input`,{"aria-label":`Set premium duration in months`,value:u,onChange:e=>d(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:`Set premium`,icon:(0,W.jsx)(ie,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:S.ID,months:gt(u)}),onDone:x}),(0,W.jsx)(Z,{label:`Clear premium`,icon:(0,W.jsx)(ie,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:S.ID,months:0}),onDone:x})]})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Verification & Moderation Flags`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:n.Verified?`Clear verified`:`Set verified`,icon:(0,W.jsx)(ee,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:S.ID,verified:!n.Verified}),onDone:x}),(0,W.jsx)(hn,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-flags`,scam:n.Scam,fake:n.Fake,onDone:x}),(0,W.jsx)(gn,{id:S.ID,support:n.Support,onDone:x})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Username`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(_n,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-username`,current:S.Username,onDone:x})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Name`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(vn,{id:S.ID,path:`/api/actions/set-account-profile`,currentFirstName:S.FirstName,currentLastName:S.LastName,onDone:x})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Phone Number`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(yn,{id:S.ID,path:`/api/actions/set-account-phone`,current:S.Phone,onDone:x})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Login Email`,text:`The email used for sign-in / password-recovery, not a contact address.`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(bn,{id:S.ID,path:`/api/actions/set-account-login-email`,current:S.LoginEmail,onDone:x})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Profile Color`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(xn,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-color`,onDone:x})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Emoji Status`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(Sn,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-emoji-status`,onDone:x})})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Recent Admin Actions`,text:`Last 30 audit rows`,action:(0,W.jsx)(Je,{size:16})}),(0,W.jsx)(At,{rows:n.AuditLogs})]})]}),_&&(0,W.jsx)(sn,{kind:`user`,id:S.ID,onClose:()=>v(!1),onDone:()=>{b(e=>e+1),x()}})]})}function Tn(e){return new Date(e.getTime()-e.getTimezoneOffset()*6e4).toISOString().slice(0,16)}function En(e){return e.reduce((e,t)=>(e.devices+=t.DeviceCount,e),{devices:0})}function Dn(e){return e.reduce((e,t)=>(t.Megagroup&&(e.megagroups+=1),t.Broadcast&&(e.broadcasts+=1),t.Verified&&(e.verified+=1),e),{megagroups:0,broadcasts:0,verified:0})}var On={beforeID:0,beforeActiveUS:0};function kn({navigate:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(50),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)([]),[d,f]=(0,g.useState)(On),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(``);async function v(e,t){m(!0),_(``);let n=new URLSearchParams({limit:String(r)});e.trim()&&n.set(`q`,e.trim()),(t.beforeID||t.beforeActiveUS)&&(n.set(`before_id`,String(t.beforeID)),n.set(`before_active_us`,String(t.beforeActiveUS)));try{let e=await k.accounts(n);return o(e),e}catch(e){return _(O(e)),null}finally{m(!1)}}async function y(){u([]),f(On),await v(t,On)}async function b(){if(!a?.has_more)return;let e={beforeID:a.next_before_id,beforeActiveUS:a.next_before_active_us};await v(t,e)&&(u(e=>[...e,d]),f(e))}async function x(){if(l.length===0)return;let e=l[l.length-1];await v(t,e)&&(u(e=>e.slice(0,-1)),f(e))}async function S(){try{c(await k.accountStats())}catch{}}(0,g.useEffect)(()=>{y(),S()},[]);let C=En(a?.rows??[]),w=l.length>0&&!p,T=!!a?.has_more&&!p;return(0,W.jsxs)(Dt,{title:`Accounts`,eyebrow:a?.listing===!1?`Search results`:`Recently active accounts`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>e(`/accounts/shared-devices`),children:[(0,W.jsx)(V,{size:15}),` `,`Shared devices`]}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>{y(),S()},disabled:p,children:[(0,W.jsx)(qe,{size:15}),` `,`Refresh`]})]}),children:[h&&(0,W.jsx)(K,{children:h}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Total users`,value:s?String(s.total):`…`}),(0,W.jsx)(J,{label:`Online now`,value:s?String(s.online):`…`,tone:`good`}),(0,W.jsx)(J,{label:`Online device records`,value:String(C.devices)})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),y()},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:`User ID / phone / username / email / name`})]}),(0,W.jsxs)(`label`,{className:`gift-page-size`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsxs)(`select`,{value:String(r),onChange:e=>i(Number(e.target.value)),children:[(0,W.jsx)(`option`,{value:`10`,children:`10`}),(0,W.jsx)(`option`,{value:`20`,children:`20`}),(0,W.jsx)(`option`,{value:`50`,children:`50`}),(0,W.jsx)(`option`,{value:`100`,children:`100`})]})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:p,children:[p?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(B,{size:15}),` `,`Search`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void x(),disabled:!w,children:[(0,W.jsx)(_e,{size:15}),` `,`Previous page`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void b(),disabled:!T,children:[(0,W.jsx)(ve,{size:15}),` `,`Next page`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{className:`avatar-col`}),(0,W.jsx)(`th`,{children:`User ID`}),(0,W.jsx)(`th`,{children:`Phone`}),(0,W.jsx)(`th`,{children:`Username`}),(0,W.jsx)(`th`,{children:`Name`}),(0,W.jsx)(`th`,{children:`Login email`}),(0,W.jsx)(`th`,{children:`Device`}),(0,W.jsx)(`th`,{children:`Last active`}),(0,W.jsx)(`th`,{children:`Premium`}),(0,W.jsx)(`th`,{children:`Verified`}),(0,W.jsx)(`th`,{children:`Frozen`}),(0,W.jsx)(`th`,{children:`Updated`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[a?.rows.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`avatar-col`,children:(0,W.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/accounts/${t.ID}`),"aria-label":`Open account ${t.ID}`,children:(0,W.jsx)(fn,{id:t.ID,firstName:t.FirstName,lastName:t.LastName,username:t.Username})})}),(0,W.jsx)(`td`,{className:`mono`,children:t.ID}),(0,W.jsx)(`td`,{children:dt(t.Phone)}),(0,W.jsx)(`td`,{children:(0,W.jsx)(Nt,{username:t.Username,collectibles:t.Collectibles})}),(0,W.jsx)(`td`,{children:ft(t)}),(0,W.jsx)(`td`,{children:t.LoginEmail||(0,W.jsx)(`span`,{className:`muted-cell`,children:`None`})}),(0,W.jsx)(`td`,{children:t.DeviceCount}),(0,W.jsx)(`td`,{children:U(t.LastActiveAt)}),(0,W.jsx)(`td`,{children:t.PremiumUntil>0?(0,W.jsxs)(q,{tone:`good`,children:[`Premium`,` `,mt(t.PremiumUntil)]}):(0,W.jsx)(q,{children:`None`})}),(0,W.jsxs)(`td`,{children:[t.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Not verified`}),` `,(0,W.jsx)(mn,{scam:t.Scam,fake:t.Fake})]}),(0,W.jsx)(`td`,{children:t.Frozen?(0,W.jsx)(q,{tone:`danger`,children:`Frozen`}):(0,W.jsx)(q,{children:`Normal`})}),(0,W.jsx)(`td`,{children:U(t.UpdatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${t.ID}`),children:[`Details`,` `,(0,W.jsx)(ve,{size:14})]})})]},t.ID)),(!a||a.rows.length===0)&&(0,W.jsx)(jt,{colSpan:12})]})]})})]})}function An({navigate:e}){let[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(!1),[a,o]=(0,g.useState)(0),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(e=!1){c(!0),u(``);let t=new URLSearchParams({limit:`20`,offset:String(e?a:0)});try{let r=await k.sharedDeviceGroups(t),a=r.rows??[];n(t=>e?[...t,...a]:a),o(r.next_offset),i(!!r.has_more)}catch(e){u(O(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d(!1)},[]);let f=t.reduce((e,t)=>e+t.AccountCount,0);return(0,W.jsxs)(Dt,{title:`Shared Devices`,eyebrow:`Multi-account signal — device/IP overlap across different accounts`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>e(`/accounts`),children:[(0,W.jsx)(ue,{size:15}),` `,`Back to accounts`]}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>d(!1),disabled:s,children:[(0,W.jsx)(qe,{size:15,className:s?`spin`:``}),` `,`Refresh`]})]}),children:[l&&(0,W.jsx)(K,{children:l}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Device groups on page`,value:String(t.length)}),(0,W.jsx)(J,{label:`Accounts flagged on page`,value:String(f),tone:`warn`})]}),(0,W.jsxs)(`p`,{className:`about-text`,children:[`Each card below is a device fingerprint (device model + OS + platform + IP) that more than one account has authorized from. `,`device_model/system_version are self-reported by the client, and IP alone can collide innocently -- use this as a lead, not a verdict.`]}),(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[t.map(t=>(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:t.DeviceModel||`Unknown device`,text:`${t.Platform||`unknown platform`} ${t.SystemVersion} · ${t.IP} · last active ${U(t.LastActiveAt)}`,action:(0,W.jsxs)(q,{tone:`warn`,children:[(0,W.jsx)(V,{size:12}),` `,`${t.AccountCount} accounts`]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{className:`avatar-col`}),(0,W.jsx)(`th`,{children:`User ID`}),(0,W.jsx)(`th`,{children:`Phone`}),(0,W.jsx)(`th`,{children:`Username`}),(0,W.jsx)(`th`,{children:`Name`}),(0,W.jsx)(`th`,{children:`Active from this device`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsx)(`tbody`,{children:t.Accounts.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`avatar-col`,children:(0,W.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/accounts/${t.UserID}`),"aria-label":`Open account ${t.UserID}`,children:(0,W.jsx)(fn,{id:t.UserID,firstName:t.FirstName,lastName:t.LastName,username:t.Username})})}),(0,W.jsx)(`td`,{className:`mono`,children:t.UserID}),(0,W.jsx)(`td`,{children:dt(t.Phone)}),(0,W.jsx)(`td`,{children:H(t.Username)||`-`}),(0,W.jsx)(`td`,{children:ft(t)||`-`}),(0,W.jsx)(`td`,{children:U(t.ActiveAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${t.UserID}`),children:[`Details`,` `,(0,W.jsx)(ve,{size:14})]})})]},t.UserID))})]})})]},`${t.DeviceModel}|${t.SystemVersion}|${t.Platform}|${t.IP}`)),t.length===0&&(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsx)(`table`,{className:`data-table`,children:(0,W.jsx)(`tbody`,{children:(0,W.jsx)(jt,{colSpan:7})})})})]}),r&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>d(!0),disabled:s,children:[s?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(ge,{size:15}),` `,`Load more`]})})]})}function jn({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim());try{o((await k.accounts(e)).rows)}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{d()},[]),(0,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,W.jsx)(ut,{size:13}),` `,`Clear`]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(he,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:ft(t)}),(0,W.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,W.jsx)(`span`,{children:H(t.Username)||dt(t.Phone)||`-`})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Search user_id / phone / username`}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,W.jsx)(I,{size:14,className:`spin`}):`Search`})]}),l&&(0,W.jsx)(`div`,{className:`picker-error`,children:l}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>(0,W.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:ft(e)}),(0,W.jsx)(`span`,{children:H(e.Username)||dt(e.Phone)||`-`}),e.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Regular`})]},e.ID)),a.length===0&&!s?(0,W.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function Mn({label:e,selected:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim());try{o((await k.accounts(e)).rows)}catch(e){u(O(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d()},[]);function f(e){t.some(t=>t.ID===e.ID)?n(t.filter(t=>t.ID!==e.ID)):n([...t,e])}function p(e){n(t.filter(t=>t.ID!==e))}return(0,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t.length>0?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n([]),children:[(0,W.jsx)(ut,{size:13}),` `,`Clear all`]}):null]}),t.length>0?(0,W.jsx)(`div`,{className:`picker-chip-list`,children:t.map(e=>(0,W.jsxs)(`span`,{className:`picker-chip`,children:[ft(e),` `,(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`button`,{type:`button`,onClick:()=>p(e.ID),"aria-label":`Remove ${e.ID}`,children:(0,W.jsx)(ut,{size:12})})]},e.ID))}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Search user_id / phone / username`}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,W.jsx)(I,{size:14,className:`spin`}):`Search`})]}),l&&(0,W.jsx)(`div`,{className:`picker-error`,children:l}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>{let n=t.some(t=>t.ID===e.ID);return(0,W.jsxs)(`button`,{className:`picker-row ${n?`selected`:``}`,type:`button`,onClick:()=>f(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:ft(e)}),(0,W.jsx)(`span`,{children:H(e.Username)||dt(e.Phone)||`-`}),n?(0,W.jsx)(he,{size:15}):null]},e.ID)}),a.length===0&&!s?(0,W.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function Nn({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim().replace(/^@/,``));try{o((await k.bots(e)).rows??[])}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{d()},[]),(0,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,W.jsx)(ut,{size:13}),` `,`Clear`]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(he,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:t.FirstName||`-`}),(0,W.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,W.jsx)(`span`,{children:H(t.Username)||`-`})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Bot username or id`}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,W.jsx)(I,{size:14,className:`spin`}):`Search`})]}),l&&(0,W.jsx)(`div`,{className:`picker-error`,children:l}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>(0,W.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:e.FirstName||`-`}),(0,W.jsx)(`span`,{children:H(e.Username)||`-`}),e.System?(0,W.jsx)(q,{tone:`warn`,children:`System`}):(0,W.jsx)(q,{children:`Regular`})]},e.ID)),a.length===0&&!s?(0,W.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function Pn({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim());try{o((await k.channels(e)).rows)}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{d()},[]),(0,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,W.jsx)(ut,{size:13}),` `,`Clear`]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(he,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:t.Title||`-`}),(0,W.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,W.jsx)(`span`,{children:H(t.Username)||pt(t)})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Search channel_id / username / title`}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,W.jsx)(I,{size:14,className:`spin`}):`Search`})]}),l&&(0,W.jsx)(`div`,{className:`picker-error`,children:l}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>(0,W.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:e.Title||`-`}),(0,W.jsx)(`span`,{children:H(e.Username)||pt(e)}),e.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:pt(e)})]},e.ID)),a.length===0&&!s?(0,W.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function Fn({onClose:e,onMinted:t}){let[n,r]=(0,g.useState)(`vault`),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(`XTR`),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!1),[_,v]=(0,g.useState)(`TON`),[y,b]=(0,g.useState)(``),[x,S]=(0,g.useState)(!1),[C,w]=(0,g.useState)(``),[T,E]=(0,g.useState)(``),[D,O]=(0,g.useState)(``),k=Ct(f,u),A=m?Ct(y,_):`0`,j=k===null,M=m&&A===null,N=c.trim()!==``&&f.trim()!==``&&!j&&!M&&(n===`vault`||(n===`user`?i!==null:o!==null));function P(){let e={username:c.trim().replace(/^@/,``),currency:u,amount:k??`0`};if(n===`user`&&i&&(e.owner_user_id=String(i.ID)),n===`channel`&&o&&(e.owner_channel_id=String(o.ID)),m&&(e.crypto_currency=_,e.crypto_amount=A??`0`),C.trim()&&(e.url=C.trim()),T){let t=Date.parse(`${T}T${D||`00:00`}:00Z`);Number.isFinite(t)&&(e.purchase_date=Math.floor(t/1e3))}return e}return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Mint a collectible username`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`NFT usernames`}),(0,W.jsx)(`h2`,{children:`Mint a collectible username`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsxs)(`div`,{className:`mint-field-group`,children:[(0,W.jsx)(`div`,{className:`mint-field-group-label`,children:`1. Username`}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Username`}),(0,W.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:`durov`})]})]}),(0,W.jsxs)(`div`,{className:`mint-field-group`,children:[(0,W.jsx)(`div`,{className:`mint-field-group-label`,children:`2. Owner`}),(0,W.jsxs)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Owner type`,children:[(0,W.jsxs)(`button`,{type:`button`,className:`btn ${n===`vault`?`primary`:``}`,onClick:()=>r(`vault`),children:[(0,W.jsx)(lt,{size:15}),` `,`Vault (no owner)`]}),(0,W.jsx)(`button`,{type:`button`,className:`btn ${n===`user`?`primary`:``}`,onClick:()=>r(`user`),children:`User owner`}),(0,W.jsx)(`button`,{type:`button`,className:`btn ${n===`channel`?`primary`:``}`,onClick:()=>r(`channel`),children:`Channel owner`})]}),n===`user`&&(0,W.jsx)(jn,{label:`User owner`,value:i,onChange:a}),n===`channel`&&(0,W.jsx)(Pn,{label:`Channel owner`,value:o,onChange:s}),n===`vault`&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Mints the asset unassigned; issue it to someone later from the asset page.`})]}),(0,W.jsxs)(`div`,{className:`mint-field-group`,children:[(0,W.jsx)(`div`,{className:`mint-field-group-label`,children:`3. Price`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`A record of what it was sold for -- minting doesn't charge anyone.`}),(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Currency`}),(0,W.jsxs)(`select`,{value:u,onChange:e=>d(e.target.value),children:[(0,W.jsx)(`option`,{value:`XTR`,children:`XTR`}),(0,W.jsx)(`option`,{value:`TON`,children:`TON`}),(0,W.jsx)(`option`,{value:`USD`,children:`USD`})]})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Amount (${u})`}),(0,W.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),inputMode:`decimal`,placeholder:`1000`})]})]}),f.trim()!==``&&!j&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Clients will show: ${St(k??`0`,u)}.`}),j&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not a valid ${u} amount: digits only, at most ${String(yt(u))} decimal places.`}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` Also record a TON price`]}),m&&(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Crypto currency`}),(0,W.jsx)(`select`,{value:_,onChange:e=>v(e.target.value),children:(0,W.jsx)(`option`,{value:`TON`,children:`TON`})})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Crypto amount (${_})`}),(0,W.jsx)(`input`,{value:y,onChange:e=>b(e.target.value),inputMode:`decimal`,placeholder:`12.5`})]})]}),M&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not a valid ${_} amount: digits only, at most ${String(yt(_))} decimal places.`})]}),(0,W.jsxs)(`div`,{className:`mint-field-group`,children:[(0,W.jsx)(`button`,{type:`button`,className:`link-button`,onClick:()=>S(e=>!e),children:x?`Hide marketplace record`:`+ Add marketplace record (optional)`}),x&&(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Marketplace URL`}),(0,W.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:`https://fragment.com/username/durov`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Purchase date (UTC)`}),(0,W.jsx)(`input`,{value:T,onChange:e=>E(e.target.value),type:`date`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Purchase time (UTC)`}),(0,W.jsx)(`input`,{value:D,onChange:e=>O(e.target.value),type:`time`,step:60,disabled:!T})]})]})]})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`}),(0,W.jsx)(Z,{disabled:!N,label:`Mint username`,icon:(0,W.jsx)(We,{size:15}),tone:`neutral`,path:`/api/actions/mint-collectible-username`,payload:P,onDone:t})]})]})}),document.body)}function In({navigate:e}){let[t,n]=(0,g.useState)(`all`),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(`50`),[s,c]=(0,g.useState)([]),[l,u]=(0,g.useState)(!1),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(!1);async function b(e=!1){m(!0),_(``);let n=new URLSearchParams({limit:a});t!==`all`&&n.set(`status`,t),r.trim()&&n.set(`q`,r.trim().replace(/^@/,``)),e&&d&&n.set(`before_id`,d);try{let t=await k.collectibleUsernames(n),r=t.rows??[];c(t=>e?[...t,...r]:r),f(t.next_before_id??``),u(!!t.has_more)}catch(e){_(O(e))}finally{m(!1)}}(0,g.useEffect)(()=>{b(!1)},[]);let x=s.filter(e=>e.Status===`vault`).length,S=s.filter(e=>e.Status===`owned`).length,C=s.filter(e=>e.Status===`burned`).length;return(0,W.jsxs)(Dt,{title:`Collectible usernames`,eyebrow:`NFT usernames / Registry`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>y(!0),children:[(0,W.jsx)(We,{size:15}),` `,`Mint username`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>b(!1),disabled:p,children:[(0,W.jsx)(qe,{size:15,className:p?`spin`:``}),` `,`Refresh`]})]}),children:[h&&(0,W.jsx)(K,{children:h}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Loaded rows`,value:String(s.length)}),(0,W.jsx)(J,{label:`In vault`,value:String(x)}),(0,W.jsx)(J,{label:`Held by owners`,value:String(S),tone:`good`}),(0,W.jsx)(J,{label:`Burned`,value:String(C),tone:C?`danger`:`neutral`})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),b(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`Search by username`})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Status`}),(0,W.jsxs)(`select`,{value:t,onChange:e=>n(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All statuses`}),(0,W.jsx)(`option`,{value:`vault`,children:`Vault`}),(0,W.jsx)(`option`,{value:`owned`,children:`Owned`}),(0,W.jsx)(`option`,{value:`burned`,children:`Burned`})]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsx)(`input`,{className:`small-input`,value:a,onChange:e=>o(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:p,children:[p?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(B,{size:15}),` `,`Search`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Username`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Owner`}),(0,W.jsx)(`th`,{children:`Price`}),(0,W.jsx)(`th`,{children:`Purchase date (UTC)`}),(0,W.jsx)(`th`,{children:`Transfers`}),(0,W.jsx)(`th`,{children:`Updated`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[s.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:(0,W.jsx)(`strong`,{children:H(t.Username)})}),(0,W.jsx)(`td`,{children:(0,W.jsx)(Ln,{status:t.Status})}),(0,W.jsx)(`td`,{children:Rn(t,`Vault`)}),(0,W.jsx)(`td`,{className:`mono`,children:zn(t)}),(0,W.jsx)(`td`,{children:U(t.PurchaseDate)||`-`}),(0,W.jsx)(`td`,{className:`mono`,children:t.TransferCount}),(0,W.jsx)(`td`,{children:U(t.UpdatedAt)||`-`}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/collectible-usernames/${t.ID}`),children:[(0,W.jsx)(de,{size:14}),` `,`Details`,` `,(0,W.jsx)(ve,{size:14})]})})]},t.ID)),s.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})}),l&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>b(!0),disabled:p,children:[p?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(ge,{size:15}),` `,`Load more`]})}),v&&(0,W.jsx)(Fn,{onClose:()=>y(!1),onMinted:()=>void b(!1)})]})}function Ln({status:e}){return e===`owned`?(0,W.jsx)(q,{tone:`good`,children:`Owned`}):e===`burned`?(0,W.jsxs)(q,{tone:`danger`,children:[(0,W.jsx)(Ee,{size:12}),` `,`Burned`]}):(0,W.jsxs)(q,{children:[(0,W.jsx)(lt,{size:12}),` `,`Vault`]})}function Rn(e,t){return!e.OwnerPeerType||e.OwnerPeerID===``||e.OwnerPeerID===`0`?t:`${H(e.OwnerUsername)||e.OwnerName||e.OwnerPeerID} · ${e.OwnerPeerType}:${e.OwnerPeerID}`}function zn(e){let t=St(e.Amount,e.Currency);return e.CryptoCurrency&&e.CryptoAmount&&e.CryptoAmount!==`0`?`${t} (${St(e.CryptoAmount,e.CryptoCurrency)})`:t}function Bn({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(`user`),[f,p]=(0,g.useState)(null),[m,h]=(0,g.useState)(null);async function _(){s(!0),a(``);try{r(await k.collectibleUsername(e))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{_(),l(`profile`)},[e]),i&&!n)return(0,W.jsx)(K,{children:i});if(!n)return(0,W.jsx)(X,{label:o?`Loading collectible username…`:`Waiting for data`});let v=n.asset,y=n.transfers??[],b=`Vault`,x=!!v.OwnerPeerType&&v.OwnerPeerID!==``&&v.OwnerPeerID!==`0`,S=v.Status===`burned`;function C(){x&&t(v.OwnerPeerType===`channel`?`/channels/${v.OwnerPeerID}`:`/accounts/${v.OwnerPeerID}`)}function w(){let e={username:v.Username};return u===`user`&&f&&(e.to_user_id=String(f.ID)),u===`channel`&&m&&(e.to_channel_id=String(m.ID)),e}let T=[{key:`profile`,label:`Profile & Status`,icon:(0,W.jsx)(oe,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,W.jsx)(Xe,{size:15})}];return(0,W.jsxs)(Dt,{title:`Collectible ${H(v.Username)}`,eyebrow:`NFT usernames / Asset`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/collectible-usernames`),children:[(0,W.jsx)(ue,{size:15}),` `,`Back to list`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:_,disabled:o,children:[(0,W.jsx)(qe,{size:15,className:o?`spin`:``}),` `,`Refresh`]})]}),children:[i&&(0,W.jsx)(K,{children:i}),(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsx)(`div`,{className:`entity-head-main`,children:(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:H(v.Username)}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`Asset #${v.ID}`})]})}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(Ln,{status:v.Status}),(0,W.jsx)(q,{tone:v.TransferCount>0?`warn`:`neutral`,children:`${v.TransferCount} transfers`}),v.Status===`owned`&&(0,W.jsx)(q,{tone:v.RegistryActive?`good`:`warn`,children:v.RegistryActive?`Active in profile`:`Hidden in profile`})]})]}),(0,W.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Asset sections`,children:T.map(e=>(0,W.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Owner`,value:Rn(v,b)}),(0,W.jsx)(Y,{label:`Price`,value:zn(v),mono:!0}),(0,W.jsx)(Y,{label:`Purchase date (UTC)`,value:U(v.PurchaseDate)||`-`}),(0,W.jsx)(Y,{label:`Original owner`,value:Un(v.OriginalOwnerPeerType,v.OriginalOwnerPeerID,b,v.OriginalOwnerUsername)}),(0,W.jsx)(Y,{label:`Transfers`,value:String(v.TransferCount),mono:!0}),(0,W.jsx)(Y,{label:`Created`,value:U(v.CreatedAt)||`-`}),(0,W.jsx)(Y,{label:`Updated`,value:U(v.UpdatedAt)||`-`})]}),(0,W.jsxs)(`div`,{className:`toolbar`,children:[x&&(0,W.jsx)(`button`,{className:`row-link`,type:`button`,onClick:C,children:v.OwnerPeerType===`channel`?`Open owner channel`:`Open owner account`}),v.URL&&(0,W.jsxs)(`a`,{className:`row-link`,href:v.URL,target:`_blank`,rel:`noreferrer noopener`,children:[(0,W.jsx)(Se,{size:14}),` `,`Open marketplace page`]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Provenance history`,text:`Mint, transfer, revoke and burn events in chronological order.`,action:(0,W.jsx)(Je,{size:16})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Event`}),(0,W.jsx)(`th`,{children:`From`}),(0,W.jsx)(`th`,{children:`To`}),(0,W.jsx)(`th`,{children:`Price`}),(0,W.jsx)(`th`,{children:`Actor`}),(0,W.jsx)(`th`,{children:`Reason`}),(0,W.jsx)(`th`,{children:`Time`})]})}),(0,W.jsxs)(`tbody`,{children:[y.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:e.ID}),(0,W.jsx)(`td`,{children:(0,W.jsx)(Hn,{kind:e.Kind})}),(0,W.jsx)(`td`,{className:`mono`,children:Un(e.FromPeerType,e.FromPeerID,b,e.FromUsername)}),(0,W.jsx)(`td`,{className:`mono`,children:Un(e.ToPeerType,e.ToPeerID,b,e.ToUsername)}),(0,W.jsx)(`td`,{className:`mono`,children:e.Amount&&e.Amount!==`0`?St(e.Amount,e.Currency):`-`}),(0,W.jsx)(`td`,{children:e.Actor||`-`}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Reason||`-`}),(0,W.jsx)(`td`,{children:U(e.CreatedAt)||`-`})]},e.ID)),y.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})})]})]}),c===`actions`&&(0,W.jsx)(`div`,{className:`stacked-sections`,children:S?(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Asset Operations`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This username is burned — no further operations are possible.`})})]}):(0,W.jsxs)(`div`,{className:`action-groups`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Transfer Ownership`,text:`Sent immediately; appended to the provenance history.`}),(0,W.jsxs)(`div`,{className:`card-body`,children:[(0,W.jsxs)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Recipient type`,children:[(0,W.jsx)(`button`,{type:`button`,className:`btn ${u===`user`?`primary`:``}`,onClick:()=>d(`user`),children:`To user`}),(0,W.jsx)(`button`,{type:`button`,className:`btn ${u===`channel`?`primary`:``}`,onClick:()=>d(`channel`),children:`To channel`})]}),u===`user`?(0,W.jsx)(jn,{label:`To user`,value:f,onChange:p}):(0,W.jsx)(Pn,{label:`To channel`,value:m,onChange:h}),(0,W.jsx)(Z,{label:`Transfer`,icon:(0,W.jsx)(le,{size:15}),tone:`warn`,path:`/api/actions/transfer-collectible-username`,payload:w,onDone:_})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Revoke To Vault`,text:`Returns the username to the vault; it can be issued again later.`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(`div`,{className:`action-stack`,children:(0,W.jsx)(Z,{label:`Revoke to vault`,icon:(0,W.jsx)(at,{size:15}),tone:`warn`,path:`/api/actions/revoke-collectible-username`,payload:()=>({username:v.Username,burn:!1}),onDone:_})})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Danger Zone`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`danger-zone`,children:[(0,W.jsx)(Z,{label:`Burn permanently`,icon:(0,W.jsx)(Ee,{size:15}),tone:`danger`,path:`/api/actions/revoke-collectible-username`,payload:()=>({username:v.Username,burn:!0}),onDone:_}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Irreversible: the username is destroyed and can never be issued again.`}),(0,W.jsx)(Z,{label:`Delete record`,icon:(0,W.jsx)(it,{size:15}),tone:`danger`,path:`/api/actions/delete-collectible-username`,payload:()=>({username:v.Username}),onDone:()=>t(`/collectible-usernames`)}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Erases the asset and its ownership history, and frees the username for a fresh issue. Use this for a username issued by mistake; a burn keeps the history instead.`})]})})]})]})})]})}var Vn={mint:`Mint`,transfer:`Transfer`,burn:`Burn`,revoke:`Revoke`};function Hn({kind:e}){return(0,W.jsx)(q,{tone:e===`burn`?`danger`:e===`revoke`?`warn`:e===`mint`?`good`:`neutral`,children:Vn[e]})}function Un(e,t,n,r=``){if(!e||t===``||t===`0`)return n;let i=H(r);return i?`${i} · ${e}:${t}`:`${e}:${t}`}function Wn({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(0);async function m(){s(!0),a(``);try{r(await k.channel(e))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{m(),l(`profile`)},[e]),i)return(0,W.jsx)(K,{children:i});if(!n)return(0,W.jsx)(X,{label:o?`Loading channel detail`:`Waiting for data`});let h=n.Channel,_=[{key:`profile`,label:`Profile & Status`,icon:(0,W.jsx)(oe,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,W.jsx)(Xe,{size:15})}];return(0,W.jsxs)(Dt,{title:`${pt(h)} #${h.ID}`,eyebrow:`Channel Profile`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,W.jsx)(ue,{size:15}),` `,`Back to list`]}),children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{className:`entity-head-main`,children:[(0,W.jsxs)(`div`,{className:`avatar-edit-slot`,children:[(0,W.jsx)(fn,{id:h.ID,kind:`channel`,title:h.Title,size:64,refreshKey:f||void 0}),(0,W.jsx)(`button`,{className:`icon-btn avatar-edit-btn`,type:`button`,"aria-label":`Change avatar`,title:`Change avatar`,onClick:()=>d(!0),children:(0,W.jsx)(je,{size:13})})]}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:h.Title||`-`}),(0,W.jsxs)(`div`,{className:`entity-subtitle`,children:[H(h.Username)||`No username`,` · `,`Creator ${h.CreatorUserID}`]})]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(q,{children:pt(h)}),h.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Not verified`}),(0,W.jsx)(mn,{scam:h.Scam,fake:h.Fake}),h.Deleted?(0,W.jsx)(q,{tone:`danger`,children:`Deleted`}):(0,W.jsx)(q,{children:`Valid`})]})]}),(0,W.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Channel sections`,children:_.map(e=>(0,W.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Channel ID`,value:String(h.ID),mono:!0}),(0,W.jsx)(Y,{label:`access_hash`,value:String(h.AccessHash),mono:!0}),(0,W.jsx)(Y,{label:`Members`,value:`${h.ParticipantsCount} / Admins ${h.AdminsCount}`}),(0,W.jsx)(Y,{label:`Moderation`,value:`Banned ${h.BannedCount} / Kicked ${h.KickedCount}`}),(0,W.jsx)(Y,{label:`Channel flags`,value:`broadcast=${h.Broadcast} megagroup=${h.Megagroup} forum=${h.Forum}`}),(0,W.jsx)(Y,{label:`top / pinned / PTS`,value:`${h.TopMessageID} / ${h.PinnedMessageID} / ${h.PTS}`}),(0,W.jsx)(Y,{label:`Created`,value:mt(h.Date)||`-`}),(0,W.jsx)(Y,{label:`Updated`,value:U(h.UpdatedAt)||`-`})]}),h.About&&(0,W.jsx)(`p`,{className:`about-text`,children:h.About}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Channel Raw Row`,text:`Database read-only snapshot`}),(0,W.jsx)(Mt,{value:n.ChannelJSON})]})]}),c===`actions`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`action-groups`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Verification & Moderation Flags`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:h.Verified?`Clear verified`:`Set verified`,icon:(0,W.jsx)(ee,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:h.ID,verified:!h.Verified}),onDone:m}),(0,W.jsx)(hn,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-flags`,scam:h.Scam,fake:h.Fake,onDone:m})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Settings`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(Cn,{channel:h,onDone:m})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Username`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(_n,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-username`,current:h.Username,onDone:m})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Profile Color`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(xn,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-color`,onDone:m})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Emoji Status`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(Sn,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-emoji-status`,onDone:m})})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Recent Admin Actions`,text:`Last 30 audit rows`,action:(0,W.jsx)(Je,{size:16})}),(0,W.jsx)(At,{rows:n.AuditLogs})]})]}),u&&(0,W.jsx)(sn,{kind:`channel`,id:h.ID,onClose:()=>d(!1),onDone:()=>{p(e=>e+1),m()}})]})}var Gn={beforeID:0,beforeUpdatedUS:0};function Kn({navigate:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(50),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)([]),[l,u]=(0,g.useState)(Gn),[d,f]=(0,g.useState)(!1),[p,m]=(0,g.useState)(``);async function h(e,t){f(!0),m(``);let n=new URLSearchParams({limit:String(r)});e.trim()&&n.set(`q`,e.trim()),(t.beforeID||t.beforeUpdatedUS)&&(n.set(`before_id`,String(t.beforeID)),n.set(`before_updated_us`,String(t.beforeUpdatedUS)));try{let e=await k.channels(n);return o(e),e}catch(e){return m(O(e)),null}finally{f(!1)}}async function _(){c([]),u(Gn),await h(t,Gn)}async function v(){if(!a?.has_more)return;let e={beforeID:a.next_before_id,beforeUpdatedUS:a.next_before_updated_us};await h(t,e)&&(c(e=>[...e,l]),u(e))}async function y(){if(s.length===0)return;let e=s[s.length-1];await h(t,e)&&(c(e=>e.slice(0,-1)),u(e))}(0,g.useEffect)(()=>{_()},[]);let b=Dn(a?.rows??[]),x=s.length>0&&!d,S=!!a?.has_more&&!d;return(0,W.jsxs)(Dt,{title:`Supergroups and Channels`,eyebrow:a?.listing===!1?`Search results`:`Recently updated`,actions:(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>void _(),disabled:d,children:[(0,W.jsx)(qe,{size:15}),` `,`Refresh`]}),children:[p&&(0,W.jsx)(K,{children:p}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Entities on page`,value:String(a?.rows.length??0)}),(0,W.jsx)(J,{label:`Supergroups`,value:String(b.megagroups)}),(0,W.jsx)(J,{label:`Channels`,value:String(b.broadcasts)}),(0,W.jsx)(J,{label:`Verified`,value:String(b.verified),tone:`good`})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),_()},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:`Channel ID / username / title`})]}),(0,W.jsxs)(`label`,{className:`gift-page-size`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsxs)(`select`,{value:String(r),onChange:e=>i(Number(e.target.value)),children:[(0,W.jsx)(`option`,{value:`10`,children:`10`}),(0,W.jsx)(`option`,{value:`20`,children:`20`}),(0,W.jsx)(`option`,{value:`50`,children:`50`}),(0,W.jsx)(`option`,{value:`100`,children:`100`})]})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:d,children:[d?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(B,{size:15}),` `,`Search`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void y(),disabled:!x,children:[(0,W.jsx)(_e,{size:15}),` `,`Previous page`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void v(),disabled:!S,children:[(0,W.jsx)(ve,{size:15}),` `,`Next page`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{className:`avatar-col`}),(0,W.jsx)(`th`,{children:`Channel ID`}),(0,W.jsx)(`th`,{children:`Kind`}),(0,W.jsx)(`th`,{children:`Username`}),(0,W.jsx)(`th`,{children:`Title`}),(0,W.jsx)(`th`,{children:`Members`}),(0,W.jsx)(`th`,{children:`Admins`}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Verified`}),(0,W.jsx)(`th`,{children:`Updated`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[a?.rows.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`avatar-col`,children:(0,W.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/channels/${t.ID}`),"aria-label":`Open channel ${t.ID}`,children:(0,W.jsx)(fn,{id:t.ID,kind:`channel`,title:t.Title})})}),(0,W.jsx)(`td`,{className:`mono`,children:t.ID}),(0,W.jsx)(`td`,{children:pt(t)}),(0,W.jsx)(`td`,{children:H(t.Username)}),(0,W.jsx)(`td`,{children:t.Title}),(0,W.jsx)(`td`,{children:t.ParticipantsCount}),(0,W.jsx)(`td`,{children:t.AdminsCount}),(0,W.jsx)(`td`,{children:t.PTS}),(0,W.jsxs)(`td`,{children:[t.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Not verified`}),` `,(0,W.jsx)(mn,{scam:t.Scam,fake:t.Fake})]}),(0,W.jsx)(`td`,{children:U(t.UpdatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${t.ID}`),children:[`Details`,` `,(0,W.jsx)(ve,{size:14})]})})]},t.ID)),(!a||a.rows.length===0)&&(0,W.jsx)(jt,{colSpan:11})]})]})})]})}function qn({botID:e,onClose:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(!1),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(!1);async function u(){if(!n.trim()){s(`Please enter an operation reason`);return}a(!0),s(``),l(!1);try{let t=await k.action(`/api/actions/export-bot-token`,{command_id:``,reason:n.trim(),confirm:!0,bot_user_id:e}),r=t.details?.token;if(t.error||typeof r!=`string`||!r){s(t.error||`No token returned.`);return}await navigator.clipboard.writeText(r),l(!0)}catch(e){s(O(e))}finally{a(!1)}}return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Copy bot token`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`Bot`}),(0,W.jsx)(`h2`,{children:`Copy bot token`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:i,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsx)(`p`,{children:`The token is written straight to your clipboard and is never shown on screen. Paste it wherever it's needed right after copying.`}),(0,W.jsxs)(`label`,{className:`form-field`,children:[(0,W.jsx)(`span`,{children:`Operation reason`}),(0,W.jsx)(`textarea`,{value:n,onChange:e=>r(e.target.value),rows:3,placeholder:`Describe why this token is being retrieved`})]}),o&&(0,W.jsx)(K,{children:o}),c&&(0,W.jsx)(`div`,{className:`secret-reveal`,children:(0,W.jsxs)(`div`,{className:`secret-reveal-label`,children:[(0,W.jsx)(he,{size:14}),` `,`Token copied to clipboard.`]})})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:i,children:`Close`}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>void u(),disabled:i,children:[(0,W.jsx)(ye,{size:15}),` `,c?`Copy again`:`Copy token`]})]})]})}),document.body)}function Jn({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(0),[m,h]=(0,g.useState)(!1);async function _(){s(!0),a(``);try{r(await k.bot(e))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{_(),l(`profile`)},[e]),i)return(0,W.jsx)(K,{children:i});if(!n)return(0,W.jsx)(X,{label:o?`Loading bot detail`:`Waiting for data`});let v=n.Bot,y=[{key:`profile`,label:`Profile & Status`,icon:(0,W.jsx)(oe,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,W.jsx)(Xe,{size:15})}];return(0,W.jsxs)(Dt,{title:`Bot #${v.ID}`,eyebrow:`Bot Profile`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/bots`),children:[(0,W.jsx)(ue,{size:15}),` `,`Back to list`]}),children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{className:`entity-head-main`,children:[(0,W.jsxs)(`div`,{className:`avatar-edit-slot`,children:[(0,W.jsx)(fn,{id:v.ID,firstName:v.FirstName,username:v.Username,size:64,refreshKey:f||void 0}),(0,W.jsx)(`button`,{className:`icon-btn avatar-edit-btn`,type:`button`,"aria-label":`Change avatar`,title:`Change avatar`,onClick:()=>d(!0),children:(0,W.jsx)(je,{size:13})})]}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:v.FirstName||`Unnamed bot`}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:H(v.Username)||`No username`})]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(q,{tone:v.System?`warn`:`neutral`,children:v.System?`System`:`User`}),v.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Not verified`}),(0,W.jsx)(mn,{scam:v.Scam,fake:v.Fake})]})]}),(0,W.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Bot sections`,children:y.map(e=>(0,W.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Bot ID`,value:String(v.ID),mono:!0}),(0,W.jsx)(Y,{label:`Owner`,value:v.OwnerUserID>0?`${v.OwnerUserID} ${H(n.OwnerUsername)}`.trim():`None`}),(0,W.jsx)(Y,{label:`Type`,value:v.System?`System`:`User`}),(0,W.jsx)(Y,{label:`Updated`,value:U(v.UpdatedAt)||`-`}),(0,W.jsx)(Y,{label:`Created`,value:U(v.CreatedAt)||`-`})]}),n.About&&(0,W.jsx)(`p`,{className:`about-text`,children:n.About}),n.Description&&n.Description.trim()!==n.About.trim()&&(0,W.jsx)(`p`,{className:`about-text`,children:n.Description})]}),c===`actions`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`action-groups`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Verification & Moderation Flags`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:v.Verified?`Clear verified`:`Set verified`,icon:(0,W.jsx)(ee,{size:15}),tone:`neutral`,path:`/api/actions/set-verified`,payload:()=>({user_id:v.ID,verified:!v.Verified}),onDone:_}),(0,W.jsx)(hn,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-flags`,scam:v.Scam,fake:v.Fake,onDone:_})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Username`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(_n,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-username`,current:v.Username,onDone:_})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Profile Color`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(xn,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-color`,onDone:_})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Emoji Status`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(Sn,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-emoji-status`,onDone:_})})]}),!v.System&&(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Credentials`}),(0,W.jsxs)(`div`,{className:`card-body`,children:[(0,W.jsx)(`div`,{className:`action-stack`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>h(!0),children:[(0,W.jsx)(ye,{size:15}),` `,`Copy token`]})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Copies straight to the clipboard through a dedicated confirmation step -- the token itself is never shown on this page.`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Danger Zone`}),(0,W.jsx)(`div`,{className:`card-body`,children:v.System?(0,W.jsx)(`p`,{className:`bot-create-note`,children:`System bots are built in and cannot be deleted.`}):(0,W.jsxs)(`div`,{className:`danger-zone`,children:[(0,W.jsx)(Z,{label:`Delete bot`,icon:(0,W.jsx)(it,{size:15}),tone:`danger`,path:`/api/actions/delete-bot`,payload:()=>({bot_user_id:v.ID}),onDone:()=>t(`/bots`)}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Permanently deletes this user-created bot and invalidates its token. This cannot be undone.`})]})})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Recent Admin Actions`,text:`Last 30 audit rows`,action:(0,W.jsx)(Je,{size:16})}),(0,W.jsx)(At,{rows:n.AuditLogs})]})]}),u&&(0,W.jsx)(sn,{kind:`user`,id:v.ID,onClose:()=>d(!1),onDone:()=>{p(e=>e+1),_()}}),m&&(0,W.jsx)(qn,{botID:v.ID,onClose:()=>h(!1)})]})}function Yn({onClose:e,onCreated:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``);return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Create bot`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`Bots`}),(0,W.jsx)(`h2`,{children:`Create bot`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsx)(`p`,{children:`Provision a bot account owned by the given user. The token is shown once after confirmation.`}),(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Owner user ID`}),(0,W.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),type:`number`,min:`1`,placeholder:`123456789`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Display name`}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`e.g. Service Bot`,maxLength:64})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Username`}),(0,W.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:`my_service_bot`})]})]}),(0,W.jsx)(`span`,{className:`bot-create-note`,children:`Username must be 5-32 characters and end with 'bot'.`})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`}),(0,W.jsx)(Z,{label:`Create bot`,icon:(0,W.jsx)(We,{size:15}),tone:`neutral`,path:`/api/actions/create-bot`,payload:()=>({owner_user_id:gt(n),name:i.trim(),username:o.trim().replace(/^@/,``)}),secretField:`token`,onDone:t})]})]})}),document.body)}var Xn={beforeID:0};function Zn({navigate:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(50),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)([]),[l,u]=(0,g.useState)(Xn),[d,f]=(0,g.useState)(!1),[p,m]=(0,g.useState)(``),[h,_]=(0,g.useState)(!1);async function v(e,t){f(!0),m(``);let n=new URLSearchParams({limit:String(r)});e.trim()&&n.set(`q`,e.trim()),t.beforeID&&n.set(`before_id`,String(t.beforeID));try{let e=await k.bots(n);return o(e),e}catch(e){return m(O(e)),null}finally{f(!1)}}async function y(){c([]),u(Xn),await v(t,Xn)}async function b(){if(!a?.has_more)return;let e={beforeID:a.next_before_id};await v(t,e)&&(c(e=>[...e,l]),u(e))}async function x(){if(s.length===0)return;let e=s[s.length-1];await v(t,e)&&(c(e=>e.slice(0,-1)),u(e))}(0,g.useEffect)(()=>{y()},[]);let S=a?.rows??[],C=S.filter(e=>e.Verified).length,w=S.filter(e=>e.System).length,T=s.length>0&&!d,E=!!a?.has_more&&!d;return(0,W.jsxs)(Dt,{title:`Bots`,eyebrow:a?.listing===!1?`Search results`:`Recently created bots`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>_(!0),children:[(0,W.jsx)(We,{size:15}),` `,`Create bot`]}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>void y(),disabled:d,children:[(0,W.jsx)(qe,{size:15}),` `,`Refresh`]})]}),children:[p&&(0,W.jsx)(K,{children:p}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Bots on page`,value:String(S.length)}),(0,W.jsx)(J,{label:`Verified`,value:String(C),tone:`good`}),(0,W.jsx)(J,{label:`System`,value:String(w)})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),y()},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:`Bot ID / username`})]}),(0,W.jsxs)(`label`,{className:`gift-page-size`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsxs)(`select`,{value:String(r),onChange:e=>i(Number(e.target.value)),children:[(0,W.jsx)(`option`,{value:`10`,children:`10`}),(0,W.jsx)(`option`,{value:`20`,children:`20`}),(0,W.jsx)(`option`,{value:`50`,children:`50`}),(0,W.jsx)(`option`,{value:`100`,children:`100`})]})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:d,children:[d?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(B,{size:15}),` `,`Search`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void x(),disabled:!T,children:[(0,W.jsx)(_e,{size:15}),` `,`Previous page`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void b(),disabled:!E,children:[(0,W.jsx)(ve,{size:15}),` `,`Next page`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{className:`avatar-col`}),(0,W.jsx)(`th`,{children:`Bot ID`}),(0,W.jsx)(`th`,{children:`Username`}),(0,W.jsx)(`th`,{children:`Name`}),(0,W.jsx)(`th`,{children:`Owner`}),(0,W.jsx)(`th`,{children:`Verified`}),(0,W.jsx)(`th`,{children:`Type`}),(0,W.jsx)(`th`,{children:`Created`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[S.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`avatar-col`,children:(0,W.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/bots/${t.ID}`),"aria-label":`Open bot ${t.ID}`,children:(0,W.jsx)(fn,{id:t.ID,firstName:t.FirstName,username:t.Username})})}),(0,W.jsx)(`td`,{className:`mono`,children:t.ID}),(0,W.jsx)(`td`,{children:H(t.Username)||`-`}),(0,W.jsx)(`td`,{children:t.FirstName||`-`}),(0,W.jsx)(`td`,{className:`mono`,children:t.OwnerUserID>0?t.OwnerUserID:`-`}),(0,W.jsxs)(`td`,{children:[t.Verified?(0,W.jsxs)(q,{tone:`good`,children:[(0,W.jsx)(ee,{size:12}),` `,`Verified`]}):(0,W.jsx)(q,{children:`Not verified`}),` `,(0,W.jsx)(mn,{scam:t.Scam,fake:t.Fake})]}),(0,W.jsx)(`td`,{children:t.System?(0,W.jsx)(q,{tone:`warn`,children:`System`}):(0,W.jsx)(q,{children:`User`})}),(0,W.jsx)(`td`,{children:U(t.CreatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/bots/${t.ID}`),children:[(0,W.jsx)(L,{size:14}),` `,`Details`,` `,(0,W.jsx)(ve,{size:14})]})})]},t.ID)),S.length===0&&(0,W.jsx)(jt,{colSpan:9})]})]})}),h&&(0,W.jsx)(Yn,{onClose:()=>_(!1),onCreated:()=>void y()})]})}function Qn({onClose:e,onCreated:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`all`),[o,s]=(0,g.useState)([]),c=(0,g.useMemo)(()=>!n.trim()||i===`selected`&&o.length===0,[n,i,o]);return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Send broadcast`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`Broadcasts`}),(0,W.jsx)(`h2`,{children:`Send broadcast`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsx)(`p`,{children:`Sends a message from the official system account (777000) to all users or to a chosen list. Delivery happens in the background and may take a few minutes for large audiences.`}),(0,W.jsxs)(`label`,{className:`form-field`,children:[(0,W.jsx)(`span`,{children:`Message`}),(0,W.jsx)(`textarea`,{value:n,onChange:e=>r(e.target.value),rows:5,maxLength:4096,placeholder:`What's new...`})]}),(0,W.jsx)(`div`,{className:`bot-create-fields`,children:(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Target`}),(0,W.jsxs)(`select`,{value:i,onChange:e=>a(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All users`}),(0,W.jsx)(`option`,{value:`selected`,children:`Selected users`})]})]})}),i===`selected`&&(0,W.jsx)(Mn,{label:`Recipients`,selected:o,onChange:s})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`}),(0,W.jsx)(Z,{label:`Send broadcast`,icon:(0,W.jsx)(Ye,{size:15}),tone:`neutral`,path:`/api/actions/create-broadcast`,disabled:c,payload:()=>({message:n.trim(),target_mode:i,user_ids:i===`selected`?o.map(e=>e.ID):void 0}),onDone:t})]})]})}),document.body)}var $n={beforeID:0};function er(){let[e,t]=(0,g.useState)(null),[n,r]=(0,g.useState)([]),[i,a]=(0,g.useState)($n),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(!1);async function f(e){s(!0),l(``);let n=new URLSearchParams({limit:`50`});e.beforeID&&n.set(`before_id`,String(e.beforeID));try{let e=await k.broadcasts(n);return t(e),e}catch(e){return l(O(e)),null}finally{s(!1)}}async function p(){r([]),a($n),await f($n)}async function m(){if(!e?.has_more)return;let t={beforeID:e.next_before_id};await f(t)&&(r(e=>[...e,i]),a(t))}async function h(){if(n.length===0)return;let e=n[n.length-1];await f(e)&&(r(e=>e.slice(0,-1)),a(e))}(0,g.useEffect)(()=>{p()},[]);let _=e?.rows??[],v=_.filter(e=>e.SentCount+e.FailedCount0&&!o,b=!!e?.has_more&&!o;return(0,W.jsxs)(Dt,{title:`Broadcasts`,eyebrow:`Announcements sent from the official system account`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>d(!0),children:[(0,W.jsx)(Ye,{size:15}),` `,`Send broadcast`]}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>void p(),disabled:o,children:[(0,W.jsx)(qe,{size:15}),` `,`Refresh`]})]}),children:[c&&(0,W.jsx)(K,{children:c}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Campaigns on page`,value:String(_.length)}),(0,W.jsx)(J,{label:`Still delivering`,value:String(v),tone:v>0?`warn`:`neutral`})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Message`}),(0,W.jsx)(`th`,{children:`Target`}),(0,W.jsx)(`th`,{children:`Sent`}),(0,W.jsx)(`th`,{children:`Failed`}),(0,W.jsx)(`th`,{children:`Total`}),(0,W.jsx)(`th`,{children:`Created by`}),(0,W.jsx)(`th`,{children:`Created`})]})}),(0,W.jsxs)(`tbody`,{children:[_.map(e=>{let t=e.SentCount+e.FailedCount,n=e.TotalCount>0&&t>=e.TotalCount;return(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:e.ID}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Message}),(0,W.jsx)(`td`,{children:e.TargetMode===`all`?(0,W.jsx)(q,{tone:`warn`,children:`All users`}):(0,W.jsx)(q,{children:`Selected`})}),(0,W.jsx)(`td`,{children:e.SentCount}),(0,W.jsx)(`td`,{children:e.FailedCount>0?(0,W.jsx)(q,{tone:`danger`,children:e.FailedCount}):e.FailedCount}),(0,W.jsx)(`td`,{children:e.TotalCount}),(0,W.jsx)(`td`,{children:e.CreatedBy||`-`}),(0,W.jsxs)(`td`,{children:[U(e.CreatedAt),!n&&(0,W.jsx)(q,{tone:`warn`,children:`Sending`})]})]},e.ID)}),_.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})}),(0,W.jsxs)(`div`,{className:`toolbar`,children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void h(),disabled:!y,children:[(0,W.jsx)(_e,{size:15}),` `,`Previous page`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void m(),disabled:!b,children:[o?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(ve,{size:15}),` `,`Next page`]})]}),u&&(0,W.jsx)(Qn,{onClose:()=>d(!1),onCreated:()=>void p()})]})}function tr({navigate:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)(``);(0,g.useEffect)(()=>{let e=!1;async function t(){try{let t=await k.dashboard();e||n(t)}catch(t){e||i(t instanceof Error?t.message:`Failed to load dashboard`)}}t();let r=window.setInterval(()=>void t(),15e3);return()=>{e=!0,window.clearInterval(r)}},[]);let a=t?.counts,o=t?.storage,s=t?.host;return(0,W.jsxs)(`div`,{className:`dashboard-layout`,children:[r&&(0,W.jsx)(K,{children:r}),(0,W.jsxs)(nr,{title:`Needs attention`,children:[(0,W.jsx)(rr,{icon:(0,W.jsx)(Te,{}),label:`Pending reports`,value:a?_t(String(a.PendingReports)):`…`,tone:a&&a.PendingReports>0?`warn`:`good`,href:`/moderation`,navigate:e}),(0,W.jsx)(rr,{icon:(0,W.jsx)(ee,{}),label:`Verification requests`,value:a?_t(String(a.PendingVerifications)):`…`,tone:a&&a.PendingVerifications>0?`warn`:`good`,href:`/verification`,navigate:e})]}),(0,W.jsxs)(nr,{title:`People & chats`,children:[(0,W.jsx)(rr,{icon:(0,W.jsx)(ct,{}),label:`Users`,value:a?_t(String(a.Users)):`…`,href:`/accounts`,navigate:e}),(0,W.jsx)(rr,{icon:(0,W.jsx)(ce,{}),label:`Online now`,value:a?_t(String(a.OnlineUsers)):`…`,sub:`last 5 min`,href:`/accounts`,navigate:e}),(0,W.jsx)(rr,{icon:(0,W.jsx)(L,{}),label:`Bots`,value:a?_t(String(a.Bots)):`…`,href:`/bots`,navigate:e}),(0,W.jsx)(rr,{icon:(0,W.jsx)(Ke,{}),label:`Channels`,value:a?_t(String(a.BroadcastChannels)):`…`,href:`/channels`,navigate:e}),(0,W.jsx)(rr,{icon:(0,W.jsx)(se,{}),label:`Supergroups`,value:a?_t(String(a.Supergroups)):`…`,href:`/channels`,navigate:e})]}),(0,W.jsxs)(nr,{title:`Content`,children:[(0,W.jsx)(rr,{icon:(0,W.jsx)(nt,{}),label:`Sticker packs`,value:a?_t(String(a.StickerSets)):`…`,href:`/stickers`,navigate:e}),(0,W.jsx)(rr,{icon:(0,W.jsx)(et,{}),label:`Emoji packs`,value:a?_t(String(a.EmojiSets)):`…`,href:`/emoji`,navigate:e}),(0,W.jsx)(rr,{icon:(0,W.jsx)(we,{}),label:`GIFs`,value:a?_t(String(a.Gifs)):`…`,sub:`saved by users`,href:`/gif-catalog`,navigate:e}),(0,W.jsx)(rr,{icon:(0,W.jsx)(xe,{}),label:`Media storage used`,value:o?wt(o.PhysicalBytes):`…`,sub:o?`${o.BackendKind} backend`:void 0,href:`/storage`,navigate:e})]}),(0,W.jsxs)(nr,{title:`Server health`,hint:s?.Ready?void 0:`waiting for first sample…`,children:[(0,W.jsx)(ir,{icon:(0,W.jsx)(be,{}),label:`CPU load`,percent:s?.Ready?s.CPUPercent:void 0,valueText:s?.Ready?`${s.CPUPercent.toFixed(0)}%`:`…`}),(0,W.jsx)(ir,{icon:(0,W.jsx)(Le,{}),label:`RAM used`,percent:s?.Ready&&s.MemTotalBytes>0?s.MemUsedBytes/s.MemTotalBytes*100:void 0,valueText:s?.Ready?wt(String(s.MemUsedBytes)):`…`,sub:s?.Ready?`of ${wt(String(s.MemTotalBytes))}`:void 0}),(0,W.jsx)(ir,{icon:(0,W.jsx)(Oe,{}),label:`Disk free`,percent:s?.Ready&&s.DiskTotalBytes>0?(s.DiskTotalBytes-s.DiskFreeBytes)/s.DiskTotalBytes*100:void 0,valueText:s?.Ready?wt(String(s.DiskFreeBytes)):`…`,sub:s?.Ready?`of ${wt(String(s.DiskTotalBytes))}`:void 0,warnAbove:85})]})]})}function nr({title:e,hint:t,children:n}){return(0,W.jsxs)(`div`,{className:`dashboard-section`,children:[(0,W.jsxs)(`div`,{className:`dashboard-section-title`,children:[e,t&&(0,W.jsx)(`span`,{children:t})]}),(0,W.jsx)(`div`,{className:`dashboard-grid`,children:n})]})}function rr({icon:e,label:t,value:n,sub:r,tone:i=`neutral`,href:a,navigate:o}){let s=i===`neutral`?``:` ${i}`,c=(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`div`,{className:`stat-tile-head`,children:[(0,W.jsx)(`span`,{className:`stat-tile-icon`,children:e}),i===`warn`&&(0,W.jsx)(ae,{size:15,className:`stat-tile-open`})]}),(0,W.jsx)(`div`,{className:`stat-tile-value`,children:n}),(0,W.jsx)(`div`,{className:`stat-tile-label`,children:t}),r&&(0,W.jsx)(`div`,{className:`stat-tile-sub`,children:r})]});return a&&o?(0,W.jsx)(`a`,{className:`stat-tile clickable${s}`,href:a,onClick:e=>{e.preventDefault(),o(a)},children:c}):(0,W.jsx)(`div`,{className:`stat-tile${s}`,children:c})}function ir({icon:e,label:t,percent:n,valueText:r,sub:i,warnAbove:a=90}){let o=n===void 0?0:Math.max(0,Math.min(100,n)),s=n===void 0?`neutral`:n>=a?`danger`:n>=a-15?`warn`:`neutral`;return(0,W.jsxs)(`div`,{className:`stat-tile${s===`neutral`?``:` ${s}`}`,children:[(0,W.jsx)(`div`,{className:`stat-tile-head`,children:(0,W.jsx)(`span`,{className:`stat-tile-icon`,children:e})}),(0,W.jsx)(`div`,{className:`stat-tile-value`,children:r}),(0,W.jsx)(`div`,{className:`stat-tile-label`,children:t}),i&&(0,W.jsx)(`div`,{className:`stat-tile-sub`,children:i}),(0,W.jsx)(`div`,{className:`stat-tile-bar`,children:(0,W.jsx)(`span`,{style:{width:`${o}%`}})})]})}function ar({channelID:e,msgID:t,navigate:n}){let[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``);async function s(){o(``);try{i(await k.groupMessage(e,t))}catch(e){o(O(e))}}if((0,g.useEffect)(()=>{s()},[e,t]),a)return(0,W.jsx)(K,{children:a});if(!r)return(0,W.jsx)(X,{label:`Loading`});let c=r.Message;return(0,W.jsx)(Dt,{title:`Group Message #${c.ID}`,eyebrow:`Message Detail`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,W.jsx)(ue,{size:15}),` `,`Back to group messages`]}),children:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:`Channel / Group ${c.ChannelID}`}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`Sender ${c.SenderUserID} · ${mt(c.Date)}`})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[c.Deleted?(0,W.jsx)(q,{tone:`danger`,children:`Deleted`}):(0,W.jsx)(q,{children:`Live`}),c.Pinned&&(0,W.jsx)(q,{tone:`warn`,children:`Pinned`}),c.Post&&(0,W.jsx)(q,{children:`Channel post`}),(0,W.jsxs)(q,{children:[`pts `,c.PTS]})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Message ID`,value:String(c.ID),mono:!0}),(0,W.jsx)(Y,{label:`Channel / Group`,value:String(c.ChannelID),mono:!0}),(0,W.jsx)(Y,{label:`From Peer`,value:`${c.FromPeerType}:${c.FromPeerID}`,mono:!0}),(0,W.jsx)(Y,{label:`Views`,value:String(c.ViewsCount)})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Channel Message Row`,text:`channel_messages read-only snapshot`}),(0,W.jsx)(Mt,{value:r.MessageJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Channel Row`,text:`channels read-only snapshot`}),(0,W.jsx)(Mt,{value:r.ChannelJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Channel Update Events`,text:`durable channel_update_events`}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Count`}),(0,W.jsx)(`th`,{children:`Type`}),(0,W.jsx)(`th`,{children:`Message ID`}),(0,W.jsx)(`th`,{children:`Sender`}),(0,W.jsx)(`th`,{children:`Time`})]})}),(0,W.jsxs)(`tbody`,{children:[r.UpdateEvents.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.PTSCount}),(0,W.jsx)(`td`,{children:e.Type}),(0,W.jsx)(`td`,{children:e.MessageID}),(0,W.jsx)(`td`,{children:e.SenderUserID}),(0,W.jsx)(`td`,{children:mt(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),r.UpdateEvents.length===0&&(0,W.jsx)(jt,{colSpan:6})]})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Event JSON`}),(0,W.jsxs)(`div`,{className:`raw-grid`,children:[r.UpdateEvents.map(e=>(0,W.jsx)(Mt,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),r.UpdateEvents.length===0&&(0,W.jsx)(`div`,{className:`empty-panel`,children:`No results`})]})]})]})})}function or({navigate:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(`100`),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(``);async function p(e=!1){if(f(``),!t){f(`Search and select a supergroup or channel first`);return}let n=new URLSearchParams({channel_id:String(t.ID),limit:s});if(e&&l?.rows.length){let e=l.rows[l.rows.length-1];n.set(`before_date`,String(e.Date)),n.set(`before_id`,String(e.ID)),i(String(e.Date)),o(String(e.ID))}else r&&n.set(`before_date`,r),a&&n.set(`before_id`,a);try{u(await k.groupMessages(n))}catch(e){f(O(e))}}function m(e){n(e),i(``),o(``),u(null)}let h=l?.rows??[];return(0,W.jsxs)(Dt,{title:`Group Messages`,eyebrow:`Supergroup / channel messages`,children:[d&&(0,W.jsx)(K,{children:d}),(0,W.jsxs)(Ot,{children:[(0,W.jsx)(`div`,{className:`message-selector-grid single`,children:(0,W.jsx)(Pn,{label:`Channel / Group`,value:t,onChange:m})}),(0,W.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),p(!1)},children:[(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`before_date cursor`}),(0,W.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`before_msg_id cursor`}),(0,W.jsx)(`input`,{className:`small-input`,value:s,onChange:e=>c(e.target.value),placeholder:`limit <= 100`}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,W.jsx)(B,{size:15}),` `,`Search messages`]}),h.length?(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>p(!0),children:[(0,W.jsx)(ve,{size:15}),` `,`Next page`]}):null]})]}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Messages on page`,value:String(h.length)}),(0,W.jsx)(J,{label:`With media`,value:String(h.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,W.jsx)(J,{label:`Channel posts`,value:String(h.filter(e=>e.Post).length)}),(0,W.jsx)(J,{label:`Channel / Group`,value:t?`${t.Title||pt(t)} (${t.ID})`:`-`})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Message ID`}),(0,W.jsx)(`th`,{children:`Time`}),(0,W.jsx)(`th`,{children:`Sender`}),(0,W.jsx)(`th`,{children:`From Peer`}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Views`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Body`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[h.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:t.ID}),(0,W.jsx)(`td`,{children:mt(t.Date)}),(0,W.jsx)(`td`,{className:`mono`,children:t.SenderUserID}),(0,W.jsxs)(`td`,{className:`mono`,children:[t.FromPeerType,`:`,t.FromPeerID]}),(0,W.jsx)(`td`,{children:t.PTS}),(0,W.jsx)(`td`,{children:t.ViewsCount}),(0,W.jsx)(`td`,{children:t.Deleted?(0,W.jsx)(q,{tone:`danger`,children:`Deleted`}):t.Pinned?(0,W.jsx)(q,{tone:`warn`,children:`Pinned`}):(0,W.jsx)(q,{children:`Live`})}),(0,W.jsx)(`td`,{className:`truncate`,children:t.Body}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${t.ChannelID}&msg_id=${t.ID}`),children:[`Details`,` `,(0,W.jsx)(ve,{size:14})]})})]},`${t.ChannelID}-${t.ID}`)),h.length===0&&(0,W.jsx)(jt,{colSpan:9})]})]})})]})}function sr({ownerUserID:e,msgID:t,navigate:n}){let[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``);async function s(){o(``);try{i(await k.message(e,t))}catch(e){o(O(e))}}if((0,g.useEffect)(()=>{s()},[e,t]),a)return(0,W.jsx)(K,{children:a});if(!r)return(0,W.jsx)(X,{label:`Loading`});let c=r.Message;return(0,W.jsx)(Dt,{title:`Message #${c.BoxID}`,eyebrow:`Message Detail`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,W.jsx)(ue,{size:15}),` `,`Back to private messages`]}),children:(0,W.jsx)(kt,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:`Owner ${c.OwnerUserID} · Peer ${c.PeerID}`}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`Sender ${c.FromUserID} · ${mt(c.Date)}`})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[c.Deleted?(0,W.jsx)(q,{tone:`danger`,children:`Deleted`}):(0,W.jsx)(q,{children:`Live`}),(0,W.jsxs)(q,{children:[`pts `,c.PTS]}),(0,W.jsx)(q,{children:c.Outgoing?`Outgoing`:`Incoming`})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Message box ID`,value:String(c.BoxID),mono:!0}),(0,W.jsx)(Y,{label:`Private message ID`,value:String(c.PrivateMessageID),mono:!0}),(0,W.jsx)(Y,{label:`Message sender`,value:String(c.MessageSenderID),mono:!0}),(0,W.jsx)(Y,{label:`Time`,value:mt(c.Date)})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Message Box`,text:`message_boxes read-only snapshot`}),(0,W.jsx)(Mt,{value:r.MessageJSON})]}),(0,W.jsxs)(`div`,{className:`raw-grid`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Dialog Row`,text:`dialogs read-only snapshot`}),(0,W.jsx)(Mt,{value:r.DialogJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Private Message Row`,text:`private_messages read-only snapshot`}),(0,W.jsx)(Mt,{value:r.PrivateJSON})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Update Events`,text:`durable user_update_events`}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Count`}),(0,W.jsx)(`th`,{children:`Type`}),(0,W.jsx)(`th`,{children:`Time`})]})}),(0,W.jsxs)(`tbody`,{children:[r.UpdateEvents.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.PTSCount}),(0,W.jsx)(`td`,{children:e.Type}),(0,W.jsx)(`td`,{children:mt(e.Date)})]},`${e.PTS}-${e.Type}`)),r.UpdateEvents.length===0&&(0,W.jsx)(jt,{colSpan:4})]})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Dispatch Queue`,text:`online/offline dispatch_outbox`}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`User ID`}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Type`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Attempts`}),(0,W.jsx)(`th`,{children:`Updated`})]})}),(0,W.jsxs)(`tbody`,{children:[r.Outbox.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.ID}),(0,W.jsx)(`td`,{children:e.TargetUserID}),(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.EventType}),(0,W.jsx)(`td`,{children:e.Status}),(0,W.jsx)(`td`,{children:e.Attempts}),(0,W.jsx)(`td`,{children:U(e.UpdatedAt)})]},e.ID)),r.Outbox.length===0&&(0,W.jsx)(jt,{colSpan:7})]})]})})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:`Operations`}),(0,W.jsx)(Z,{label:`Delete this message`,icon:(0,W.jsx)(it,{size:15}),path:`/api/actions/delete-messages`,payload:()=>({owner_user_id:c.OwnerUserID,peer_id:c.PeerID,ids:[c.BoxID],revoke:!0}),onDone:s})]})})})}function cr({navigate:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(`100`),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(!0),[h,_]=(0,g.useState)(!1),[v,y]=(0,g.useState)(``),[b,x]=(0,g.useState)(`1`),[S,C]=(0,g.useState)(null),[w,T]=(0,g.useState)(``);async function E(e=!1){if(T(``),!t||!r){T(`Search and select the owner user and peer user first`);return}let n=new URLSearchParams({owner_user_id:String(t.ID),peer_id:String(r.ID),limit:l});if(e&&S?.rows.length){let e=S.rows[S.rows.length-1];n.set(`before_date`,String(e.Date)),n.set(`before_id`,String(e.BoxID)),o(String(e.Date)),c(String(e.BoxID))}else a&&n.set(`before_date`,a),s&&n.set(`before_id`,s);try{C(await k.messages(n))}catch(e){T(O(e))}}function D(e){n(e),o(``),c(``),C(null)}function A(e){i(e),o(``),c(``),C(null)}return(0,W.jsxs)(Dt,{title:`Private Messages`,eyebrow:`Private message boxes`,children:[w&&(0,W.jsx)(K,{children:w}),(0,W.jsxs)(Ot,{children:[(0,W.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,W.jsx)(jn,{label:`Owner user`,value:t,onChange:D}),(0,W.jsx)(jn,{label:`Peer user`,value:r,onChange:A})]}),(0,W.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),E(!1)},children:[(0,W.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`before_date cursor`}),(0,W.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`before_msg_id cursor`}),(0,W.jsx)(`input`,{className:`small-input`,value:l,onChange:e=>u(e.target.value),placeholder:`limit <= 100`}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,W.jsx)(B,{size:15}),` `,`Search messages`]}),S?.rows.length?(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>E(!0),children:[(0,W.jsx)(ve,{size:15}),` `,`Next page`]}):null]})]}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Messages on page`,value:String(S?.rows.length??0)}),(0,W.jsx)(J,{label:`Deleted`,value:String((S?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,W.jsx)(J,{label:`Outgoing`,value:String((S?.rows??[]).filter(e=>e.Outgoing).length)}),(0,W.jsx)(J,{label:`Owner / Peer`,value:t&&r?`${ft(t)} / ${ft(r)}`:`-`})]}),(0,W.jsxs)(`div`,{className:`operation-row`,children:[(0,W.jsxs)(`div`,{className:`operation-box`,children:[(0,W.jsxs)(`div`,{className:`operation-title`,children:[(0,W.jsx)(it,{size:15}),` `,`Delete selected messages`]}),(0,W.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),placeholder:`Message IDs, comma separated`}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>m(e.target.checked)}),` `,`Revoke for both sides`]}),(0,W.jsx)(Z,{path:`/api/actions/delete-messages`,label:`Dry-run delete`,payload:()=>({owner_user_id:t?.ID??0,peer_id:r?.ID??0,ids:Tt(d,`Message IDs are invalid`),revoke:p})})]}),(0,W.jsxs)(`div`,{className:`operation-box`,children:[(0,W.jsxs)(`div`,{className:`operation-title`,children:[(0,W.jsx)(ke,{size:15}),` `,`Clear private history`]}),(0,W.jsx)(`input`,{value:v,onChange:e=>y(e.target.value),placeholder:`max_id cutoff`}),(0,W.jsx)(`input`,{value:b,onChange:e=>x(e.target.value),placeholder:`max_batches`}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>m(e.target.checked)}),` `,`Revoke for both sides`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:h,onChange:e=>_(e.target.checked)}),` `,`Clear only this side`]}),(0,W.jsx)(Z,{path:`/api/actions/delete-history`,label:`Dry-run clear history`,payload:()=>({owner_user_id:t?.ID??0,peer_id:r?.ID??0,max_id:gt(v),max_batches:gt(b),just_clear:h,revoke:p})})]})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Message ID`}),(0,W.jsx)(`th`,{children:`Time`}),(0,W.jsx)(`th`,{children:`Sender`}),(0,W.jsx)(`th`,{children:`Direction`}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Body`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[S?.rows.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:t.BoxID}),(0,W.jsx)(`td`,{children:mt(t.Date)}),(0,W.jsx)(`td`,{className:`mono`,children:t.FromUserID}),(0,W.jsx)(`td`,{children:t.Outgoing?`Outgoing`:`Incoming`}),(0,W.jsx)(`td`,{children:t.PTS}),(0,W.jsx)(`td`,{children:t.Deleted?(0,W.jsx)(q,{tone:`danger`,children:`Deleted`}):(0,W.jsx)(q,{children:`Live`})}),(0,W.jsx)(`td`,{className:`truncate`,children:t.Body}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${t.OwnerUserID}&msg_id=${t.BoxID}`),children:[`Details`,` `,(0,W.jsx)(ve,{size:14})]})})]},`${t.OwnerUserID}-${t.BoxID}`)),(!S||S.rows.length===0)&&(0,W.jsx)(jt,{colSpan:8})]})]})})]})}var lr=c(o(((e,t)=>{typeof document<`u`&&typeof navigator<`u`&&(function(n,r){typeof e==`object`&&t!==void 0?t.exports=r():typeof define==`function`&&define.amd?define(r):(n=typeof globalThis<`u`?globalThis:n||self,n.lottie=r())})(e,(function(){var n=``,r=!1,i=-999999,a=function(e){r=!!e},o=function(){return r},s=function(e){n=e},c=function(){return n};function l(e){return document.createElement(e)}function u(e,t){var n,r=e.length,i;for(n=0;n1?n[1]=1:n[1]<=0&&(n[1]=0),F(n[0],n[1],n[2])}function re(e,t){var n=ne(e[0]*255,e[1]*255,e[2]*255);return n[2]+=t,n[2]>1?n[2]=1:n[2]<0&&(n[2]=0),F(n[0],n[1],n[2])}function ie(e,t){var n=ne(e[0]*255,e[1]*255,e[2]*255);return n[0]+=t/360,n[0]>1?--n[0]:n[0]<0&&(n[0]+=1),F(n[0],n[1],n[2])}(function(){var e=[],t,n;for(t=0;t<256;t+=1)n=t.toString(16),e[t]=n.length===1?`0`+n:n;return function(t,n,r){return t<0&&(t=0),n<0&&(n=0),r<0&&(r=0),`#`+e[t]+e[n]+e[r]}})();var ae=function(e){g=!!e},oe=function(){return g},se=function(e){_=e},ce=function(){return _},le=function(){return v},ue=function(e){E=e},de=function(){return E},fe=function(e){y=e};function L(e){return document.createElementNS(`http://www.w3.org/2000/svg`,e)}function pe(e){"@babel/helpers - typeof";return pe=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},pe(e)}var me=function(){var e=1,t=[],n,r,i={onmessage:function(){},postMessage:function(e){n({data:e})}},a={postMessage:function(e){i.onmessage({data:e})}};function s(e){if(window.Worker&&window.Blob&&o()){var t=new Blob([`var _workerSelf = self; self.onmessage = `,e.toString()],{type:`text/javascript`}),r=URL.createObjectURL(t);return new Worker(r)}return n=e,i}function c(){r||(r=s(function(e){function t(){function e(t,n){var o,s,c=t.length,l,u,d,f;for(s=0;s=0;--t)if(e[t].ty===`sh`)if(e[t].ks.k.i)a(e[t].ks.k);else for(o=e[t].ks.k.length,r=0;rn[0]?!0:n[0]>e[0]?!1:e[1]>n[1]?!0:n[1]>e[1]?!1:e[2]>n[2]?!0:n[2]>e[2]?!1:null}var s=function(){var e=[4,4,14];function t(e){var t=e.t.d;e.t.d={k:[{s:t,t:0}]}}function n(e){var n,r=e.length;for(n=0;n=0;--n)if(e[n].ty===`sh`)if(e[n].ks.k.i)e[n].ks.k.c=e[n].closed;else for(a=e[n].ks.k.length,i=0;i500)&&(this._imageLoaded(),clearInterval(n)),t+=1}.bind(this),50)}function a(t){var n=r(t,this.assetsPath,this.path),i=L(`image`);b?this.testImageLoaded(i):i.addEventListener(`load`,this._imageLoaded,!1),i.addEventListener(`error`,function(){a.img=e,this._imageLoaded()}.bind(this),!1),i.setAttributeNS(`http://www.w3.org/1999/xlink`,`href`,n),this._elementHelper.append?this._elementHelper.append(i):this._elementHelper.appendChild(i);var a={img:i,assetData:t};return a}function o(t){var n=r(t,this.assetsPath,this.path),i=l(`img`);i.crossOrigin=`anonymous`,i.addEventListener(`load`,this._imageLoaded,!1),i.addEventListener(`error`,function(){a.img=e,this._imageLoaded()}.bind(this),!1),i.src=n;var a={img:i,assetData:t};return a}function s(e){var t={assetData:e},n=r(e,this.assetsPath,this.path);return me.loadData(n,function(e){t.img=e,this._footageLoaded()}.bind(this),function(){t.img={},this._footageLoaded()}.bind(this)),t}function c(e,t){this.imagesLoadedCb=t;var n,r=e.length;for(n=0;nthis.animationData.op&&(this.animationData.op=e.op,this.totalFrames=Math.floor(e.op-this.animationData.ip));var t=this.animationData.layers,n,r=t.length,i=e.layers,a,o=i.length;for(a=0;athis.timeCompleted&&(this.currentFrame=this.timeCompleted),this.trigger(`enterFrame`),this.renderFrame(),this.trigger(`drawnFrame`)},R.prototype.renderFrame=function(){if(!(this.isLoaded===!1||!this.renderer))try{this.expressionsPlugin&&this.expressionsPlugin.resetFrame(),this.renderer.renderFrame(this.currentFrame+this.firstFrame)}catch(e){this.triggerRenderFrameError(e)}},R.prototype.play=function(e){e&&this.name!==e||this.isPaused===!0&&(this.isPaused=!1,this.trigger(`_play`),this.audioController.resume(),this._idle&&(this._idle=!1,this.trigger(`_active`)))},R.prototype.pause=function(e){e&&this.name!==e||this.isPaused===!1&&(this.isPaused=!0,this.trigger(`_pause`),this._idle=!0,this.trigger(`_idle`),this.audioController.pause())},R.prototype.togglePause=function(e){e&&this.name!==e||(this.isPaused===!0?this.play():this.pause())},R.prototype.stop=function(e){e&&this.name!==e||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},R.prototype.getMarkerData=function(e){for(var t,n=0;n=this.totalFrames-1&&this.frameModifier>0?!this.loop||this.playCount===this.loop?this.checkSegments(t>this.totalFrames?t%this.totalFrames:0)||(n=!0,t=this.totalFrames-1):t>=this.totalFrames?(this.playCount+=1,this.checkSegments(t%this.totalFrames)||(this.setCurrentRawFrameValue(t%this.totalFrames),this._completedLoop=!0,this.trigger(`loopComplete`))):this.setCurrentRawFrameValue(t):t<0?this.checkSegments(t%this.totalFrames)||(this.loop&&!(this.playCount--<=0&&this.loop!==!0)?(this.setCurrentRawFrameValue(this.totalFrames+t%this.totalFrames),this._completedLoop?this.trigger(`loopComplete`):this._completedLoop=!0):(n=!0,t=0)):this.setCurrentRawFrameValue(t),n&&(this.setCurrentRawFrameValue(t),this.pause(),this.trigger(`complete`))}},R.prototype.adjustSegment=function(e,t){this.playCount=0,e[1]0&&(this.playSpeed<0?this.setSpeed(-this.playSpeed):this.setDirection(-1)),this.totalFrames=e[0]-e[1],this.timeCompleted=this.totalFrames,this.firstFrame=e[1],this.setCurrentRawFrameValue(this.totalFrames-.001-t)):e[1]>e[0]&&(this.frameModifier<0&&(this.playSpeed<0?this.setSpeed(-this.playSpeed):this.setDirection(1)),this.totalFrames=e[1]-e[0],this.timeCompleted=this.totalFrames,this.firstFrame=e[0],this.setCurrentRawFrameValue(.001+t)),this.trigger(`segmentStart`)},R.prototype.setSegment=function(e,t){var n=-1;this.isPaused&&(this.currentRawFrame+this.firstFramet&&(n=t-e)),this.firstFrame=e,this.totalFrames=t-e,this.timeCompleted=this.totalFrames,n!==-1&&this.goToAndStop(n,!0)},R.prototype.playSegments=function(e,t){if(t&&(this.segments.length=0),Ce(e[0])===`object`){var n,r=e.length;for(n=0;n=0;--n)t[n].animation.destroy(e)}function T(e,t,n){var r=[].concat([].slice.call(document.getElementsByClassName(`lottie`)),[].slice.call(document.getElementsByClassName(`bodymovin`))),i,a=r.length;for(i=0;i0?n=c:t=c;while(Math.abs(s)>a&&++l=i?g(e,d,t,n):f===0?d:h(e,a,a+c,t,n)}},e}(),Ee=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),De=function(){return function(e,t,n){var r=0,i=e,a=m(i),o={newElement:s,release:c};function s(){var e;return r?(--r,e=a[r]):e=t(),e}function c(e){r===i&&(a=Ee.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),Oe=function(){function e(){return{addedLength:0,percents:p(`float32`,de()),lengths:p(`float32`,de())}}return De(8,e)}(),ke=function(){function e(){return{lengths:[],totalLength:0}}function t(e){var t,n=e.lengths.length;for(t=0;t-.001&&o<.001}function n(n,r,i,a,o,s,c,l,u){if(i===0&&s===0&&u===0)return t(n,r,a,o,c,l);var d=e.sqrt(e.pow(a-n,2)+e.pow(o-r,2)+e.pow(s-i,2)),f=e.sqrt(e.pow(c-n,2)+e.pow(l-r,2)+e.pow(u-i,2)),p=e.sqrt(e.pow(c-a,2)+e.pow(l-o,2)+e.pow(u-s,2)),m=d>f?d>p?d-f-p:p-f-d:p>f?p-f-d:f-d-p;return m>-1e-4&&m<1e-4}var r=function(){return function(e,t,n,r){var i=de(),a,o,s,c,l,u=0,d,f=[],p=[],m=Oe.newElement();for(s=n.length,a=0;ao?-1:1,l=!0;l;)if(r[a]<=o&&r[a+1]>o?(s=(o-r[a])/(r[a+1]-r[a]),l=!1):a+=c,a<0||a>=i-1){if(a===i-1)return n[a];l=!1}return n[a]+(n[a+1]-n[a])*s}function l(t,n,r,i,a,o){var s=c(a,o),l=1-s;return[e.round((l*l*l*t[0]+(s*l*l+l*s*l+l*l*s)*r[0]+(s*s*l+l*s*s+s*l*s)*i[0]+s*s*s*n[0])*1e3)/1e3,e.round((l*l*l*t[1]+(s*l*l+l*s*l+l*l*s)*r[1]+(s*s*l+l*s*s+s*l*s)*i[1]+s*s*s*n[1])*1e3)/1e3]}var u=p(`float32`,8);function d(t,n,r,i,a,o,s){a<0?a=0:a>1&&(a=1);var l=c(a,s);o=o>1?1:o;var d=c(o,s),f,p=t.length,m=1-l,h=1-d,g=m*m*m,_=l*m*m*3,v=l*l*m*3,y=l*l*l,b=m*m*h,x=l*m*h+m*l*h+m*m*d,S=l*l*h+m*l*d+l*m*d,C=l*l*d,w=m*h*h,T=l*h*h+m*d*h+m*h*d,E=l*d*h+m*d*d+l*h*d,D=l*d*d,O=h*h*h,k=d*h*h+h*d*h+h*h*d,A=d*d*h+h*d*d+d*h*d,j=d*d*d;for(f=0;f=l.t-n){c.h&&(c=l),i=0;break}if(l.t-n>e){i=a;break}a=v||e=v?x.points.length-1:0;for(f=x.points[S].point.length,d=0;d=T&&C=v)r[0]=b[0],r[1]=b[1],r[2]=b[2];else if(e<=y)r[0]=c.s[0],r[1]=c.s[1],r[2]=c.s[2];else{var j=Le(c.s),M=Le(b),N=(e-y)/(v-y);Ie(r,Fe(j,M,N))}else for(a=0;a=v?m=1:e1e-6?(f=Math.acos(p),m=Math.sin(f),h=Math.sin((1-n)*f)/m,g=Math.sin(n*f)/m):(h=1-n,g=n),r[0]=h*i+g*c,r[1]=h*a+g*l,r[2]=h*o+g*u,r[3]=h*s+g*d,r}function Ie(e,t){var n=t[0],r=t[1],i=t[2],a=t[3],o=Math.atan2(2*r*a-2*n*i,1-2*r*r-2*i*i),s=Math.asin(2*n*r+2*i*a),c=Math.atan2(2*n*a-2*r*i,1-2*n*n-2*i*i);e[0]=o/D,e[1]=s/D,e[2]=c/D}function Le(e){var t=e[0]*D,n=e[1]*D,r=e[2]*D,i=Math.cos(t/2),a=Math.cos(n/2),o=Math.cos(r/2),s=Math.sin(t/2),c=Math.sin(n/2),l=Math.sin(r/2),u=i*a*o-s*c*l;return[s*c*o+i*a*l,s*a*o+i*c*l,i*c*o-s*a*l,u]}function Re(){var e=this.comp.renderedFrame-this.offsetTime,t=this.keyframes[0].t-this.offsetTime,n=this.keyframes[this.keyframes.length-1].t-this.offsetTime;if(!(e===this._caching.lastFrame||this._caching.lastFrame!==Me&&(this._caching.lastFrame>=n&&e>=n||this._caching.lastFrame=e&&(this._caching._lastKeyframeIndex=-1,this._caching.lastIndex=0);var r=this.interpolateValue(e,this._caching);this.pv=r}return this._caching.lastFrame=e,this.pv}function ze(e){var t;if(this.propType===`unidimensional`)t=e*this.mult,Ne(this.v-t)>1e-5&&(this.v=t,this._mdf=!0);else for(var n=0,r=this.v.length;n1e-5&&(this.v[n]=t,this._mdf=!0),n+=1}function Be(){if(!(this.elem.globalData.frameId===this.frameId||!this.effectsSequence.length)){if(this.lock){this.setVValue(this.pv);return}this.lock=!0,this._mdf=this._isFirstFrame;var e,t=this.effectsSequence.length,n=this.kf?this.pv:this.data.k;for(e=0;e=this._maxLength&&this.doubleArrayLength(),n){case`v`:a=this.v;break;case`i`:a=this.i;break;case`o`:a=this.o;break;default:a=[];break}(!a[r]||a[r]&&!i)&&(a[r]=qe.newElement()),a[r][0]=e,a[r][1]=t},Je.prototype.setTripleAt=function(e,t,n,r,i,a,o,s){this.setXYAt(e,t,`v`,o,s),this.setXYAt(n,r,`o`,o,s),this.setXYAt(i,a,`i`,o,s)},Je.prototype.reverse=function(){var e=new Je;e.setPathData(this.c,this._length);var t=this.v,n=this.o,r=this.i,i=0;this.c&&(e.setTripleAt(t[0][0],t[0][1],r[0][0],r[0][1],n[0][0],n[0][1],0,!1),i=1);var a=this._length-1,o=this._length,s;for(s=i;s=p[p.length-1].t-this.offsetTime)i=p[p.length-1].s?p[p.length-1].s[0]:p[p.length-2].e[0],o=!0;else{for(var m=r,h=p.length-1,g=!0,_,v,y;g&&(_=p[m],v=p[m+1],!(v.t-this.offsetTime>e));)m=v.t-this.offsetTime)d=1;else if(e<_.t-this.offsetTime)d=0;else{var b;y.__fnct?b=y.__fnct:(b=Te.getBezierEasing(_.o.x,_.o.y,_.i.x,_.i.y).get,y.__fnct=b),d=b((e-(_.t-this.offsetTime))/(v.t-this.offsetTime-(_.t-this.offsetTime)))}a=v.s?v.s[0]:_.e[0]}i=_.s[0]}for(l=t._length,u=i.i[0].length,n.lastIndex=r,s=0;sr&&t>r)||(this._caching.lastIndex=i0||e>-1e-6&&e<0?r(e*t)/t:e}function P(){var e=this.props,t=N(e[0]),n=N(e[1]),r=N(e[4]),i=N(e[5]),a=N(e[12]),o=N(e[13]);return`matrix(`+t+`,`+n+`,`+r+`,`+i+`,`+a+`,`+o+`)`}return function(){this.reset=i,this.rotate=a,this.rotateX=o,this.rotateY=s,this.rotateZ=c,this.skew=u,this.skewFromAxis=d,this.shear=l,this.scale=f,this.setTransform=m,this.translate=h,this.transform=g,this.multiply=_,this.applyToPoint=S,this.applyToX=C,this.applyToY=w,this.applyToZ=T,this.applyToPointArray=A,this.applyToTriplePoints=k,this.applyToPointStringified=j,this.toCSS=M,this.to2dCSS=P,this.clone=b,this.cloneFromProps=x,this.equals=y,this.inversePoints=O,this.inversePoint=D,this.getInverseMatrix=E,this._t=this.transform,this.isIdentity=v,this._identity=!0,this._identityCalculated=!1,this.props=p(`float32`,16),this.reset()}}();function $e(e){"@babel/helpers - typeof";return $e=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},$e(e)}var V={},et=`__[STANDALONE]__`,tt=`__[ANIMATIONDATA]__`,nt=``;function rt(e){s(e)}function it(){et===!0?we.searchAnimations(tt,et,nt):we.searchAnimations()}function at(e){ae(e)}function ot(e){fe(e)}function st(e){return et===!0&&(e.animationData=JSON.parse(tt)),we.loadAnimation(e)}function ct(e){if(typeof e==`string`)switch(e){case`high`:ue(200);break;default:case`medium`:ue(50);break;case`low`:ue(10);break}else!isNaN(e)&&e>1&&ue(e)}function lt(){return typeof navigator<`u`}function ut(e,t){e===`expressions`&&se(t)}function dt(e){switch(e){case`propertyFactory`:return z;case`shapePropertyFactory`:return Ze;case`matrix`:return Qe;default:return null}}V.play=we.play,V.pause=we.pause,V.setLocationHref=rt,V.togglePause=we.togglePause,V.setSpeed=we.setSpeed,V.setDirection=we.setDirection,V.stop=we.stop,V.searchAnimations=it,V.registerAnimation=we.registerAnimation,V.loadAnimation=st,V.setSubframeRendering=at,V.resize=we.resize,V.goToAndStop=we.goToAndStop,V.destroy=we.destroy,V.setQuality=ct,V.inBrowser=lt,V.installPlugin=ut,V.freeze=we.freeze,V.unfreeze=we.unfreeze,V.setVolume=we.setVolume,V.mute=we.mute,V.unmute=we.unmute,V.getRegisteredAnimations=we.getRegisteredAnimations,V.useWebWorker=a,V.setIDPrefix=ot,V.__getFactory=dt,V.version=`5.13.0`;function H(){document.readyState===`complete`&&(clearInterval(ht),it())}function ft(e){for(var t=pt.split(`&`),n=0;n=1?a.push({s:e-1,e:t-1}):(a.push({s:e,e:1}),a.push({s:0,e:t-1}));var o=[],s,c=a.length,l;for(s=0;sr+n)){var u=l.s*i<=r?0:(l.s*i-r)/n,d=l.e*i>=r+n?1:(l.e*i-r)/n;o.push([u,d])}return o.length||o.push([0,0]),o},vt.prototype.releasePathsData=function(e){var t,n=e.length;for(t=0;t1?1+r:this.s.v<0?0+r:this.s.v+r,n=this.e.v>1?1+r:this.e.v<0?0+r:this.e.v+r,t>n){var i=t;t=n,n=i}t=Math.round(t*1e4)*1e-4,n=Math.round(n*1e4)*1e-4,this.sValue=t,this.eValue=n}else t=this.sValue,n=this.eValue;var a,o,s=this.shapes.length,c,l,u,d,f,p=0;if(n===t)for(o=0;o=0;--o)if(h=this.shapes[o],h.shape._mdf){for(g=h.localShapeCollection,g.releaseShapes(),this.m===2&&s>1?(b=this.calculateShapeEdges(t,n,h.totalShapeLength,y,p),y+=h.totalShapeLength):b=[[_,v]],l=b.length,c=0;c=1?m.push({s:h.totalShapeLength*(_-1),e:h.totalShapeLength*(v-1)}):(m.push({s:h.totalShapeLength*_,e:h.totalShapeLength}),m.push({s:0,e:h.totalShapeLength*(v-1)}));var x=this.addShapes(h,m[0]);if(m[0].s!==m[0].e){if(m.length>1)if(h.shape.paths.shapes[h.shape.paths._length-1].c){var S=x.pop();this.addPaths(x,g),x=this.addShapes(h,m[1],S)}else this.addPaths(x,g),x=this.addShapes(h,m[1]);this.addPaths(x,g)}}h.shape.paths=g}}else if(this._mdf)for(o=0;ot.e){n.c=!1;break}else t.s<=l&&t.e>=l+u.addedLength?(this.addSegment(i[a].v[s-1],i[a].o[s-1],i[a].i[s],i[a].v[s],n,d,g),g=!1):(p=je.getNewSegment(i[a].v[s-1],i[a].v[s],i[a].o[s-1],i[a].i[s],(t.s-l)/u.addedLength,(t.e-l)/u.addedLength,f[s-1]),this.addSegmentFromArray(p,n,d,g),g=!1,n.c=!1),l+=u.addedLength,d+=1;if(i[a].c&&f.length){if(u=f[s-1],l<=t.e){var _=f[s-1].addedLength;t.s<=l&&t.e>=l+_?(this.addSegment(i[a].v[s-1],i[a].o[s-1],i[a].i[0],i[a].v[0],n,d,g),g=!1):(p=je.getNewSegment(i[a].v[s-1],i[a].v[0],i[a].o[s-1],i[a].i[0],(t.s-l)/_,(t.e-l)/_,f[s-1]),this.addSegmentFromArray(p,n,d,g),g=!1,n.c=!1)}else n.c=!1;l+=u.addedLength,d+=1}if(n._length&&(n.setXYAt(n.v[h][0],n.v[h][1],`i`,h),n.setXYAt(n.v[n._length-1][0],n.v[n._length-1][1],`o`,n._length-1)),l>t.e)break;a=this.p.keyframes[this.p.keyframes.length-1].t?(r=this.p.getValueAtTime(this.p.keyframes[this.p.keyframes.length-1].t/n,0),i=this.p.getValueAtTime((this.p.keyframes[this.p.keyframes.length-1].t-.05)/n,0)):(r=this.p.pv,i=this.p.getValueAtTime((this.p._caching.lastFrame+this.p.offsetTime-.01)/n,this.p.offsetTime));else if(this.px&&this.px.keyframes&&this.py.keyframes&&this.px.getValueAtTime&&this.py.getValueAtTime){r=[],i=[];var a=this.px,o=this.py;a._caching.lastFrame+a.offsetTime<=a.keyframes[0].t?(r[0]=a.getValueAtTime((a.keyframes[0].t+.01)/n,0),r[1]=o.getValueAtTime((o.keyframes[0].t+.01)/n,0),i[0]=a.getValueAtTime(a.keyframes[0].t/n,0),i[1]=o.getValueAtTime(o.keyframes[0].t/n,0)):a._caching.lastFrame+a.offsetTime>=a.keyframes[a.keyframes.length-1].t?(r[0]=a.getValueAtTime(a.keyframes[a.keyframes.length-1].t/n,0),r[1]=o.getValueAtTime(o.keyframes[o.keyframes.length-1].t/n,0),i[0]=a.getValueAtTime((a.keyframes[a.keyframes.length-1].t-.01)/n,0),i[1]=o.getValueAtTime((o.keyframes[o.keyframes.length-1].t-.01)/n,0)):(r=[a.pv,o.pv],i[0]=a.getValueAtTime((a._caching.lastFrame+a.offsetTime-.01)/n,a.offsetTime),i[1]=o.getValueAtTime((o._caching.lastFrame+o.offsetTime-.01)/n,o.offsetTime))}else i=e,r=i;this.v.rotate(-Math.atan2(r[1]-i[1],r[0]-i[0]))}this.data.p&&this.data.p.s?this.data.p.z?this.v.translate(this.px.v,this.py.v,-this.pz.v):this.v.translate(this.px.v,this.py.v,0):this.v.translate(this.p.v[0],this.p.v[1],-this.p.v[2])}this.frameId=this.elem.globalData.frameId}}function r(){if(this.appliedTransformations=0,this.pre.reset(),!this.a.effectsSequence.length)this.pre.translate(-this.a.v[0],-this.a.v[1],this.a.v[2]),this.appliedTransformations=1;else return;if(!this.s.effectsSequence.length)this.pre.scale(this.s.v[0],this.s.v[1],this.s.v[2]),this.appliedTransformations=2;else return;if(this.sk)if(!this.sk.effectsSequence.length&&!this.sa.effectsSequence.length)this.pre.skewFromAxis(-this.sk.v,this.sa.v),this.appliedTransformations=3;else return;this.r?this.r.effectsSequence.length||(this.pre.rotate(-this.r.v),this.appliedTransformations=4):!this.rz.effectsSequence.length&&!this.ry.effectsSequence.length&&!this.rx.effectsSequence.length&&!this.or.effectsSequence.length&&(this.pre.rotateZ(-this.rz.v).rotateY(this.ry.v).rotateX(this.rx.v).rotateZ(-this.or.v[2]).rotateY(this.or.v[1]).rotateX(this.or.v[0]),this.appliedTransformations=4)}function i(){}function a(e){this._addDynamicProperty(e),this.elem.addDynamicProperty(e),this._isDirty=!0}function o(e,t,n){if(this.elem=e,this.frameId=-1,this.propType=`transform`,this.data=t,this.v=new Qe,this.pre=new Qe,this.appliedTransformations=0,this.initDynamicPropertyContainer(n||e),t.p&&t.p.s?(this.px=z.getProp(e,t.p.x,0,0,this),this.py=z.getProp(e,t.p.y,0,0,this),t.p.z&&(this.pz=z.getProp(e,t.p.z,0,0,this))):this.p=z.getProp(e,t.p||{k:[0,0,0]},1,0,this),t.rx){if(this.rx=z.getProp(e,t.rx,0,D,this),this.ry=z.getProp(e,t.ry,0,D,this),this.rz=z.getProp(e,t.rz,0,D,this),t.or.k[0].ti){var r,i=t.or.k.length;for(r=0;r0;)--n,this._elements.unshift(t[n]);this.dynamicProperties.length?this.k=!0:this.getValue(!0)},xt.prototype.resetElements=function(e){var t,n=e.length;for(t=0;t0?Math.floor(f):Math.ceil(f),h=this.pMatrix.props,g=this.rMatrix.props,_=this.sMatrix.props;this.pMatrix.reset(),this.rMatrix.reset(),this.sMatrix.reset(),this.tMatrix.reset(),this.matrix.reset();var v=0;if(f>0){for(;vm;)this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!0),--v;p&&(this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,-p,!0),v-=p)}r=this.data.m===1?0:this._currentCopies-1,i=this.data.m===1?1:-1,a=this._currentCopies;for(var y,b;a;){if(t=this.elemsData[r].it,n=t[t.length-1].transform.mProps.v.props,b=n.length,t[t.length-1].transform.mProps._mdf=!0,t[t.length-1].transform.op._mdf=!0,t[t.length-1].transform.op.v=this._currentCopies===1?this.so.v:this.so.v+(this.eo.v-this.so.v)*(r/(this._currentCopies-1)),v!==0){for((r!==0&&i===1||r!==this._currentCopies-1&&i===-1)&&this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!1),this.matrix.transform(g[0],g[1],g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15]),this.matrix.transform(_[0],_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13],_[14],_[15]),this.matrix.transform(h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7],h[8],h[9],h[10],h[11],h[12],h[13],h[14],h[15]),y=0;y0&&r<1?[t]:[]:[t-r,t+r].filter(function(e){return e>0&&e<1})},kt.prototype.split=function(e){if(e<=0)return[Ot(this.points[0]),this];if(e>=1)return[this,Ot(this.points[this.points.length-1])];var t=Et(this.points[0],this.points[1],e),n=Et(this.points[1],this.points[2],e),r=Et(this.points[2],this.points[3],e),i=Et(t,n,e),a=Et(n,r,e),o=Et(i,a,e);return[new kt(this.points[0],t,i,o,!0),new kt(o,a,r,this.points[3],!0)]};function G(e,t){var n=e.points[0][t],r=e.points[e.points.length-1][t];if(n>r){var i=r;r=n,n=i}for(var a=W(3*e.a[t],2*e.b[t],e.c[t]),o=0;o0&&a[o]<1){var s=e.point(a[o])[t];sr&&(r=s)}return{min:n,max:r}}kt.prototype.bounds=function(){return{x:G(this,0),y:G(this,1)}},kt.prototype.boundingBox=function(){var e=this.bounds();return{left:e.x.min,right:e.x.max,top:e.y.min,bottom:e.y.max,width:e.x.max-e.x.min,height:e.y.max-e.y.min,cx:(e.x.max+e.x.min)/2,cy:(e.y.max+e.y.min)/2}};function K(e,t,n){var r=e.boundingBox();return{cx:r.cx,cy:r.cy,width:r.width,height:r.height,bez:e,t:(t+n)/2,t1:t,t2:n}}function q(e){var t=e.bez.split(.5);return[K(t[0],e.t1,e.t),K(t[1],e.t,e.t2)]}function J(e,t){return Math.abs(e.cx-t.cx)*2=a||e.width<=r&&e.height<=r&&t.width<=r&&t.height<=r){i.push([e.t,t.t]);return}var o=q(e),s=q(t);Y(o[0],s[0],n+1,r,i,a),Y(o[0],s[1],n+1,r,i,a),Y(o[1],s[0],n+1,r,i,a),Y(o[1],s[1],n+1,r,i,a)}}kt.prototype.intersections=function(e,t,n){t===void 0&&(t=2),n===void 0&&(n=7);var r=[];return Y(K(this,0,1),K(e,0,1),0,t,r,n),r},kt.shapeSegment=function(e,t){var n=(t+1)%e.length();return new kt(e.v[t],e.o[t],e.i[n],e.v[n],!0)},kt.shapeSegmentInverted=function(e,t){var n=(t+1)%e.length();return new kt(e.v[n],e.i[n],e.o[t],e.v[t],!0)};function At(e,t){return[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]]}function jt(e,t,n,r){var i=[e[0],e[1],1],a=[t[0],t[1],1],o=[n[0],n[1],1],s=[r[0],r[1],1],c=At(At(i,a),At(o,s));return wt(c[2])?null:[c[0]/c[2],c[1]/c[2]]}function X(e,t,n){return[e[0]+Math.cos(t)*n,e[1]-Math.sin(t)*n]}function Mt(e,t){return Math.hypot(e[0]-t[0],e[1]-t[1])}function Nt(e,t){return Ct(e[0],t[0])&&Ct(e[1],t[1])}function Pt(){}u([_t],Pt),Pt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amplitude=z.getProp(e,t.s,0,null,this),this.frequency=z.getProp(e,t.r,0,null,this),this.pointsType=z.getProp(e,t.pt,0,null,this),this._isAnimated=this.amplitude.effectsSequence.length!==0||this.frequency.effectsSequence.length!==0||this.pointsType.effectsSequence.length!==0};function Ft(e,t,n,r,i,a,o){var s=n-Math.PI/2,c=n+Math.PI/2,l=t[0]+Math.cos(n)*r*i,u=t[1]-Math.sin(n)*r*i;e.setTripleAt(l,u,l+Math.cos(s)*a,u-Math.sin(s)*a,l+Math.cos(c)*o,u-Math.sin(c)*o,e.length())}function It(e,t){var n=[t[0]-e[0],t[1]-e[1]],r=-Math.PI*.5;return[Math.cos(r)*n[0]-Math.sin(r)*n[1],Math.sin(r)*n[0]+Math.cos(r)*n[1]]}function Lt(e,t){var n=t===0?e.length()-1:t-1,r=(t+1)%e.length(),i=e.v[n],a=e.v[r],o=It(i,a);return Math.atan2(0,1)-Math.atan2(o[1],o[0])}function Rt(e,t,n,r,i,a,o){var s=Lt(t,n),c=t.v[n%t._length],l=t.v[n===0?t._length-1:n-1],u=t.v[(n+1)%t._length],d=a===2?Math.sqrt((c[0]-l[0])**2+(c[1]-l[1])**2):0,f=a===2?Math.sqrt((c[0]-u[0])**2+(c[1]-u[1])**2):0;Ft(e,t.v[n%t._length],s,o,r,f/((i+1)*2),d/((i+1)*2),a)}function zt(e,t,n,r,i,a){for(var o=0;o1&&t.length>1&&(i=Ut(e[0],t[t.length-1]),i)?[[e[0].split(i[0])[0]],[t[t.length-1].split(i[1])[1]]]:[n,r]}function Gt(e){for(var t,n=1;n1&&(t=Wt(e[e.length-1],e[0]),e[e.length-1]=t[0],e[0]=t[1]),e}function Kt(e,t){var n=e.inflectionPoints(),r,i,a,o;if(n.length===0)return[Vt(e,t)];if(n.length===1||Ct(n[1],1))return a=e.split(n[0]),r=a[0],i=a[1],[Vt(r,t),Vt(i,t)];a=e.split(n[0]),r=a[0];var s=(n[1]-n[0])/(1-n[0]);return a=a[1].split(s),o=a[0],i=a[1],[Vt(r,t),Vt(o,t),Vt(i,t)]}function qt(){}u([_t],qt),qt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amount=z.getProp(e,t.a,0,null,this),this.miterLimit=z.getProp(e,t.ml,0,null,this),this.lineJoin=t.lj,this._isAnimated=this.amount.effectsSequence.length!==0},qt.prototype.processPath=function(e,t,n,r){var i=B.newElement();i.c=e.c;var a=e.length();e.c||--a;var o,s,c,l=[];for(o=0;o=0;--o)c=kt.shapeSegmentInverted(e,o),l.push(Kt(c,t));l=Gt(l);var u=null,d=null;for(o=0;o0&&(o=!1),o){var u=l(`style`);u.setAttribute(`f-forigin`,n[r].fOrigin),u.setAttribute(`f-origin`,n[r].origin),u.setAttribute(`f-family`,n[r].fFamily),u.type=`text/css`,u.innerText=`@font-face {font-family: `+n[r].fFamily+`; font-style: normal; src: url('`+n[r].fPath+`');}`,t.appendChild(u)}}else if(n[r].fOrigin===`g`||n[r].origin===1){for(s=document.querySelectorAll(`link[f-forigin="g"], link[f-origin="1"]`),c=0;c=55296&&n<=56319){var r=e.charCodeAt(1);r>=56320&&r<=57343&&(t=(n-55296)*1024+r-56320+65536)}return t}function S(e,t){var n=e.toString(16)+t.toString(16);return d.indexOf(n)!==-1}function C(e){return e===s}function w(e){return e===o}function T(e){var t=x(e);return t>=c&&t<=u}function E(e){return T(e.substr(0,2))&&T(e.substr(2,2))}function D(e){return t.indexOf(e)!==-1}function O(e,t){var o=x(e.substr(t,2));if(o!==n)return!1;var s=0;for(t+=2;s<5;){if(o=x(e.substr(t,2)),oa)return!1;s+=1,t+=2}return x(e.substr(t,2))===r}function k(){this.isLoaded=!0}var A=function(){this.fonts=[],this.chars=null,this.typekitLoaded=0,this.isLoaded=!1,this._warned=!1,this.initTime=Date.now(),this.setIsLoadedBinded=this.setIsLoaded.bind(this),this.checkLoadedFontsBinded=this.checkLoadedFonts.bind(this)};return A.isModifier=S,A.isZeroWidthJoiner=C,A.isFlagEmoji=E,A.isRegionalCode=T,A.isCombinedCharacter=D,A.isRegionalFlag=O,A.isVariationSelector=w,A.BLACK_FLAG_CODE_POINT=n,A.prototype={addChars:_,addFonts:g,getCharData:v,getFontByName:b,measureText:y,checkLoadedFonts:m,setIsLoaded:k},A}();function Xt(e){this.animationData=e}Xt.prototype.getProp=function(e){return this.animationData.slots&&this.animationData.slots[e.sid]?Object.assign(e,this.animationData.slots[e.sid].p):e};function Zt(e){return new Xt(e)}function Qt(){}Qt.prototype={initRenderable:function(){this.isInRange=!1,this.hidden=!1,this.isTransparent=!1,this.renderableComponents=[]},addRenderableComponent:function(e){this.renderableComponents.indexOf(e)===-1&&this.renderableComponents.push(e)},removeRenderableComponent:function(e){this.renderableComponents.indexOf(e)!==-1&&this.renderableComponents.splice(this.renderableComponents.indexOf(e),1)},prepareRenderableFrame:function(e){this.checkLayerLimits(e)},checkTransparency:function(){this.finalTransform.mProp.o.v<=0?!this.isTransparent&&this.globalData.renderConfig.hideOnTransparent&&(this.isTransparent=!0,this.hide()):this.isTransparent&&(this.isTransparent=!1,this.show())},checkLayerLimits:function(e){this.data.ip-this.data.st<=e&&this.data.op-this.data.st>e?this.isInRange!==!0&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):this.isInRange!==!1&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){var e,t=this.renderableComponents.length;for(e=0;e.1)&&this.audio.seek(this._currentTime/this.globalData.frameRate):(this.audio.play(),this.audio.seek(this._currentTime/this.globalData.frameRate),this._isPlaying=!0))},pn.prototype.show=function(){},pn.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},pn.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},pn.prototype.resume=function(){this._canPlay=!0},pn.prototype.setRate=function(e){this.audio.rate(e)},pn.prototype.volume=function(e){this._volumeMultiplier=e,this._previousVolume=e*this._volume,this.audio.volume(this._previousVolume)},pn.prototype.getBaseElement=function(){return null},pn.prototype.destroy=function(){},pn.prototype.sourceRectAtTime=function(){},pn.prototype.initExpressions=function(){};function mn(){}mn.prototype.checkLayers=function(e){var t,n=this.layers.length,r;for(this.completeLayers=!0,t=n-1;t>=0;--t)this.elements[t]||(r=this.layers[t],r.ip-r.st<=e-this.layers[t].st&&r.op-r.st>e-this.layers[t].st&&this.buildItem(t)),this.completeLayers=this.elements[t]?this.completeLayers:!1;this.checkPendingElements()},mn.prototype.createItem=function(e){switch(e.ty){case 2:return this.createImage(e);case 0:return this.createComp(e);case 1:return this.createSolid(e);case 3:return this.createNull(e);case 4:return this.createShape(e);case 5:return this.createText(e);case 6:return this.createAudio(e);case 13:return this.createCamera(e);case 15:return this.createFootage(e);default:return this.createNull(e)}},mn.prototype.createCamera=function(){throw Error(`You're using a 3d camera. Try the html renderer.`)},mn.prototype.createAudio=function(e){return new pn(e,this.globalData,this)},mn.prototype.createFootage=function(e){return new fn(e,this.globalData,this)},mn.prototype.buildAllItems=function(){var e,t=this.layers.length;for(e=0;e0&&(this.maskElement.setAttribute(`id`,p),this.element.maskedElement.setAttribute(b,`url(`+c()+`#`+p+`)`),r.appendChild(this.maskElement)),this.viewData.length&&this.element.addRenderableComponent(this)}_n.prototype.getMaskProperty=function(e){return this.viewData[e].prop},_n.prototype.renderFrame=function(e){var t=this.element.finalTransform.mat,n,r=this.masksProperties.length;for(n=0;n1&&(r+=` C`+t.o[i-1][0]+`,`+t.o[i-1][1]+` `+t.i[0][0]+`,`+t.i[0][1]+` `+t.v[0][0]+`,`+t.v[0][1]),n.lastPath!==r){var o=``;n.elem&&(t.c&&(o=e.inv?this.solidPath+r:r),n.elem.setAttribute(`d`,o)),n.lastPath=r}},_n.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var vn=function(){var e={};e.createFilter=t,e.createAlphaToLuminanceFilter=n;function t(e,t){var n=L(`filter`);return n.setAttribute(`id`,e),t!==!0&&(n.setAttribute(`filterUnits`,`objectBoundingBox`),n.setAttribute(`x`,`0%`),n.setAttribute(`y`,`0%`),n.setAttribute(`width`,`100%`),n.setAttribute(`height`,`100%`)),n}function n(){var e=L(`feColorMatrix`);return e.setAttribute(`type`,`matrix`),e.setAttribute(`color-interpolation-filters`,`sRGB`),e.setAttribute(`values`,`0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1`),e}return e}(),yn=function(){var e={maskType:!0,svgLumaHidden:!0,offscreenCanvas:typeof OffscreenCanvas<`u`};return(/MSIE 10/i.test(navigator.userAgent)||/MSIE 9/i.test(navigator.userAgent)||/rv:11.0/i.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent))&&(e.maskType=!1),/firefox/i.test(navigator.userAgent)&&(e.svgLumaHidden=!1),e}(),bn={},xn=`filter_result_`;function Sn(e){var t,n=`SourceGraphic`,r=e.data.ef?e.data.ef.length:0,i=te(),a=vn.createFilter(i,!0),o=0;this.filters=[];var s;for(t=0;t=0&&(n=this.shapeModifiers[e].processShapes(this._isFirstFrame),!n);--e);}},searchProcessedElement:function(e){for(var t=this.processedElements,n=0,r=t.length;n.01)return!1;n+=1}return!0},Ln.prototype.checkCollapsable=function(){if(this.o.length/2!=this.c.length/4)return!1;if(this.data.k.k[0].s)for(var e=0,t=this.data.k.k.length;e0;)c=r.transformers[g].mProps._mdf||c,--h,--g;if(c)for(h=f-r.styles[u].lvl,g=r.transformers.length-1;h>0;)m.multiply(r.transformers[g].mProps.v),--h,--g}else m=e;if(p=r.sh.paths,o=p._length,c){for(s=``,a=0;a=1?v=.99:v<=-1&&(v=-.99);var y=g*v,b=Math.cos(_+t.a.v)*y+a[0],x=Math.sin(_+t.a.v)*y+a[1];r.setAttribute(`fx`,b),r.setAttribute(`fy`,x),i&&!t.g._collapsable&&(t.of.setAttribute(`fx`,b),t.of.setAttribute(`fy`,x))}}}function u(e,t,n){var r=t.style,i=t.d;i&&(i._mdf||n)&&i.dashStr&&(r.pElem.setAttribute(`stroke-dasharray`,i.dashStr),r.pElem.setAttribute(`stroke-dashoffset`,i.dashoffset[0])),t.c&&(t.c._mdf||n)&&r.pElem.setAttribute(`stroke`,`rgb(`+C(t.c.v[0])+`,`+C(t.c.v[1])+`,`+C(t.c.v[2])+`)`),(t.o._mdf||n)&&r.pElem.setAttribute(`stroke-opacity`,t.o.v),(t.w._mdf||n)&&(r.pElem.setAttribute(`stroke-width`,t.w.v),r.msElem&&r.msElem.setAttribute(`stroke-width`,t.w.v))}return n}();function Wn(e,t,n){this.shapes=[],this.shapesData=e.shapes,this.stylesList=[],this.shapeModifiers=[],this.itemsData=[],this.processedElements=[],this.animatedContents=[],this.initElement(e,t,n),this.prevViewData=[]}u([un,gn,Cn,On,wn,dn,Tn],Wn),Wn.prototype.initSecondaryElement=function(){},Wn.prototype.identityMatrix=new Qe,Wn.prototype.buildExpressionInterface=function(){},Wn.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},Wn.prototype.filterUniqueShapes=function(){var e,t=this.shapes.length,n,r,i=this.stylesList.length,a,o=[],s=!1;for(r=0;r1&&s&&this.setShapesAsAnimated(o)}},Wn.prototype.setShapesAsAnimated=function(e){var t,n=e.length;for(t=0;t=0;--c){if(g=this.searchProcessedElement(e[c]),g?t[c]=n[g-1]:e[c]._render=o,e[c].ty===`fl`||e[c].ty===`st`||e[c].ty===`gf`||e[c].ty===`gs`||e[c].ty===`no`)g?t[c].style.closed=e[c].hd:t[c]=this.createStyleElement(e[c],i),e[c]._render&&t[c].style.pElem.parentNode!==r&&r.appendChild(t[c].style.pElem),f.push(t[c].style);else if(e[c].ty===`gr`){if(!g)t[c]=this.createGroupElement(e[c]);else for(d=t[c].it.length,u=0;u1,this.kf&&this.addEffect(this.getKeyframeValue.bind(this)),this.kf},Kn.prototype.addEffect=function(e){this.effectsSequence.push(e),this.elem.addDynamicProperty(this)},Kn.prototype.getValue=function(e){if(!((this.elem.globalData.frameId===this.frameId||!this.effectsSequence.length)&&!e)){this.currentData.t=this.data.d.k[this.keysIndex].s.t;var t=this.currentData,n=this.keysIndex;if(this.lock){this.setCurrentData(this.currentData);return}this.lock=!0,this._mdf=!1;var r,i=this.effectsSequence.length,a=e||this.data.d.k[this.keysIndex].s;for(r=0;rt);)n+=1;return this.keysIndex!==n&&(this.keysIndex=n),this.data.d.k[this.keysIndex].s},Kn.prototype.buildFinalText=function(e){for(var t=[],n=0,r=e.length,i,a,o=!1,s=!1,c=``;n=55296&&i<=56319?Yt.isRegionalFlag(e,n)?c=e.substr(n,14):(a=e.charCodeAt(n+1),a>=56320&&a<=57343&&(Yt.isModifier(i,a)?(c=e.substr(n,2),o=!0):c=Yt.isFlagEmoji(e.substr(n,4))?e.substr(n,4):e.substr(n,2))):i>56319?(a=e.charCodeAt(n+1),Yt.isVariationSelector(i)&&(o=!0)):Yt.isZeroWidthJoiner(i)&&(o=!0,s=!0),o?(t[t.length-1]+=c,o=!1):t.push(c),n+=c.length;return t},Kn.prototype.completeTextData=function(e){e.__complete=!0;var t=this.elem.globalData.fontManager,n=this.data,r=[],i,a,o,s=0,c,l=n.m.g,u=0,d=0,f=0,p=[],m=0,h=0,g,_,v=t.getFontByName(e.f),y,b=0,x=Jt(v);e.fWeight=x.weight,e.fStyle=x.style,e.finalSize=e.s,e.finalText=this.buildFinalText(e.t),a=e.finalText.length,e.finalLineHeight=e.lh;var S=e.tr/1e3*e.finalSize,C;if(e.sz)for(var w=!0,T=e.sz[0],E=e.sz[1],D,O;w;){O=this.buildFinalText(e.t),D=0,m=0,a=O.length,S=e.tr/1e3*e.finalSize;var k=-1;for(i=0;iT&&O[i]!==` `?(k===-1?a+=1:i=k,D+=e.finalLineHeight||e.finalSize*1.2,O.splice(i,+(k===i),`\r`),k=-1,m=0):(m+=b,m+=S);D+=v.ascent*e.finalSize/100,this.canResize&&e.finalSize>this.minimumFontSize&&Eh?m:h,m=-2*S,c=``,o=!0,f+=1):c=j,t.chars?(y=t.getCharData(j,v.fStyle,t.getFontByName(e.f).fFamily),b=o?0:y.w*e.finalSize/100):b=t.measureText(c,e.f,e.finalSize),j===` `?A+=b+S:(m+=b+S+A,A=0),r.push({l:b,an:b,add:u,n:o,anIndexes:[],val:c,line:f,animatorJustifyOffset:0}),l==2){if(u+=b,c===``||c===` `||i===a-1){for((c===``||c===` `)&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;s+=1,u=0}}else if(l==3){if(u+=b,c===``||i===a-1){for(c===``&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;u=0,s+=1}}else r[s].ind=s,r[s].extra=0,s+=1;if(e.l=r,h=m>h?m:h,p.push(m),e.sz)e.boxWidth=e.sz[0],e.justifyOffset=0;else switch(e.boxWidth=h,e.j){case 1:e.justifyOffset=-e.boxWidth;break;case 2:e.justifyOffset=-e.boxWidth/2;break;default:e.justifyOffset=0}e.lineWidths=p;var M=n.a,N,P;_=M.length;var ee,te,F=[];for(g=0;g<_;g+=1){for(N=M[g],N.a.sc&&(e.strokeColorAnim=!0),N.a.sw&&(e.strokeWidthAnim=!0),(N.a.fc||N.a.fh||N.a.fs||N.a.fb)&&(e.fillColorAnim=!0),te=0,ee=N.s.b,i=0;i0?i=this.ne.v/100:a=-this.ne.v/100,this.xe.v>0?o=1-this.xe.v/100:s=1+this.xe.v/100;var c=Te.getBezierEasing(i,a,o,s).get,l=0,u=this.finalS,d=this.finalE,f=this.data.sh;if(f===2)l=d===u?+(r>=d):e(0,t(.5/(d-u)+(r-u)/(d-u),1)),l=c(l);else if(f===3)l=d===u?r>=d?0:1:1-e(0,t(.5/(d-u)+(r-u)/(d-u),1)),l=c(l);else if(f===4)d===u?l=0:(l=e(0,t(.5/(d-u)+(r-u)/(d-u),1)),l<.5?l*=2:l=1-2*(l-.5)),l=c(l);else if(f===5){if(d===u)l=0;else{var p=d-u;r=t(e(0,r+.5-u),d-u);var m=-p/2+r,h=p/2;l=Math.sqrt(1-m*m/(h*h))}l=c(l)}else f===6?(d===u?l=0:(r=t(e(0,r+.5-u),d-u),l=(1+Math.cos(Math.PI+Math.PI*2*r/(d-u)))/2),l=c(l)):(r>=n(u)&&(l=r-u<0?e(0,t(t(d,1)-(u-r),1)):e(0,t(d-r,1))),l=c(l));if(this.sm.v!==100){var g=this.sm.v*.01;g===0&&(g=1e-8);var _=.5-g*.5;l<_?l=0:(l=(l-_)/g,l>1&&(l=1))}return l*this.a.v},getValue:function(e){this.iterateDynamicProperties(),this._mdf=e||this._mdf,this._currentTextLength=this.elem.textProperty.currentData.l.length||0,e&&this.data.r===2&&(this.e.v=this._currentTextLength);var t=this.data.r===2?1:100/this.data.totalChars,n=this.o.v/t,r=this.s.v/t+n,i=this.e.v/t+n;if(r>i){var a=r;r=i,i=a}this.finalS=r,this.finalE=i}},u([Ke],r);function i(e,t,n){return new r(e,t,n)}return{getTextSelectorProp:i}}();function Jn(e,t,n){var r={propType:!1},i=z.getProp,a=t.a;this.a={r:a.r?i(e,a.r,0,D,n):r,rx:a.rx?i(e,a.rx,0,D,n):r,ry:a.ry?i(e,a.ry,0,D,n):r,sk:a.sk?i(e,a.sk,0,D,n):r,sa:a.sa?i(e,a.sa,0,D,n):r,s:a.s?i(e,a.s,1,.01,n):r,a:a.a?i(e,a.a,1,0,n):r,o:a.o?i(e,a.o,0,.01,n):r,p:a.p?i(e,a.p,1,0,n):r,sw:a.sw?i(e,a.sw,0,0,n):r,sc:a.sc?i(e,a.sc,1,0,n):r,fc:a.fc?i(e,a.fc,1,0,n):r,fh:a.fh?i(e,a.fh,0,0,n):r,fs:a.fs?i(e,a.fs,0,.01,n):r,fb:a.fb?i(e,a.fb,0,.01,n):r,t:a.t?i(e,a.t,0,0,n):r},this.s=qn.getTextSelectorProp(e,t.s,n),this.s.t=t.s.t}function Yn(e,t,n){this._isFirstFrame=!0,this._hasMaskedPath=!1,this._frameId=-1,this._textData=e,this._renderType=t,this._elem=n,this._animatorsData=m(this._textData.a.length),this._pathData={},this._moreOptions={alignment:{}},this.renderedLetters=[],this.lettersChangedFlag=!1,this.initDynamicPropertyContainer(n)}Yn.prototype.searchProperties=function(){var e,t=this._textData.a.length,n,r=z.getProp;for(e=0;e=m+Ee||!x?(T=(m+Ee-g)/h.partialLength,oe=b.point[0]+(h.point[0]-b.point[0])*T,se=b.point[1]+(h.point[1]-b.point[1])*T,a.translate(-n[0]*f[u].an*.005,-(n[1]*A)*.01),_=!1):x&&(g+=h.partialLength,v+=1,v>=x.length&&(v=0,y+=1,S[y]?x=S[y].points:D.v.c?(v=0,y=0,x=S[y].points):(g-=h.partialLength,x=null)),x&&(b=h,h=x[v],C=h.partialLength));ae=f[u].an/2-f[u].add,a.translate(-ae,0,0)}else ae=f[u].an/2-f[u].add,a.translate(-ae,0,0),a.translate(-n[0]*f[u].an*.005,-n[1]*A*.01,0);for(P=0;Pe?this.textSpans[e].span:L(s?`g`:`text`),b<=e){if(c.setAttribute(`stroke-linecap`,`butt`),c.setAttribute(`stroke-linejoin`,`round`),c.setAttribute(`stroke-miterlimit`,`4`),this.textSpans[e].span=c,s){var S=L(`g`);c.appendChild(S),this.textSpans[e].childSpan=S}this.textSpans[e].span=c,this.layerElement.appendChild(c)}c.style.display=`inherit`}if(l.reset(),d&&(o[e].n&&(f=-g,p+=n.yOffset,p+=+!!h,h=!1),this.applyTextPropertiesToMatrix(n,l,o[e].line,f,p),f+=o[e].l||0,f+=g),s){x=this.globalData.fontManager.getCharData(n.finalText[e],r.fStyle,this.globalData.fontManager.getFontByName(n.f).fFamily);var C;if(x.t===1)C=new rr(x.data,this.globalData,this);else{var w=Zn;x.data&&x.data.shapes&&(w=this.buildShapeData(x.data,n.finalSize)),C=new Wn(w,this.globalData,this)}if(this.textSpans[e].glyph){var T=this.textSpans[e].glyph;this.textSpans[e].childSpan.removeChild(T.layerElement),T.destroy()}this.textSpans[e].glyph=C,C._debug=!0,C.prepareFrame(0),C.renderFrame(),this.textSpans[e].childSpan.appendChild(C.layerElement),x.t===1&&this.textSpans[e].childSpan.setAttribute(`transform`,`scale(`+n.finalSize/100+`,`+n.finalSize/100+`)`)}else d&&c.setAttribute(`transform`,`translate(`+l.props[12]+`,`+l.props[13]+`)`),c.textContent=o[e].val,c.setAttributeNS(`http://www.w3.org/XML/1998/namespace`,`xml:space`,`preserve`)}d&&c&&c.setAttribute(`d`,u)}for(;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].prepareFrame(e-this.layers[t].st);if(this.globalData._mdf)for(t=0;t=0;--n)(this.completeLayers||this.elements[n])&&(this.elements[n].prepareFrame(this.renderedFrame-this.layers[n].st),this.elements[n]._mdf&&(this._mdf=!0))}},nr.prototype.renderInnerContent=function(){var e,t=this.layers.length;for(e=0;e=0;--n)e.finalTransform.multiply(e.transforms[n].transform.mProps.v);e._mdf=i},processSequences:function(e){var t,n=this.sequenceList.length;for(t=0;t=1){this.buffers=[];var e=this.globalData.canvasContext,t=cr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(t);var n=cr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(n),this.data.tt>=3&&!document._isProxy&&cr.loadLumaCanvas()}this.canvasContext=this.globalData.canvasContext,this.transformCanvas=this.globalData.transformCanvas,this.renderableEffectsManager=new ur(this),this.searchEffectTransforms()},createContent:function(){},setBlendMode:function(){var e=this.globalData;if(e.blendMode!==this.data.bm){e.blendMode=this.data.bm;var t=$t(this.data.bm);e.canvasContext.globalCompositeOperation=t}},createRenderableComponents:function(){this.maskManager=new dr(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(hn.TRANSFORM_EFFECT)},hideElement:function(){!this.hidden&&(!this.isInRange||this.isTransparent)&&(this.hidden=!0)},showElement:function(){this.isInRange&&!this.isTransparent&&(this.hidden=!1,this._isFirstFrame=!0,this.maskManager._isFirstFrame=!0)},clearCanvas:function(e){e.clearRect(this.transformCanvas.tx,this.transformCanvas.ty,this.transformCanvas.w*this.transformCanvas.sx,this.transformCanvas.h*this.transformCanvas.sy)},prepareLayer:function(){if(this.data.tt>=1){var e=this.buffers[0].getContext(`2d`);this.clearCanvas(e),e.drawImage(this.canvasContext.canvas,0,0),this.currentTransform=this.canvasContext.getTransform(),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform)}},exitLayer:function(){if(this.data.tt>=1){var e=this.buffers[1],t=e.getContext(`2d`);if(this.clearCanvas(t),t.drawImage(this.canvasContext.canvas,0,0),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform),this.comp.getElementById(`tp`in this.data?this.data.tp:this.data.ind-1).renderFrame(!0),this.canvasContext.setTransform(1,0,0,1,0,0),this.data.tt>=3&&!document._isProxy){var n=cr.getLumaCanvas(this.canvasContext.canvas);n.getContext(`2d`).drawImage(this.canvasContext.canvas,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.drawImage(n,0,0)}this.canvasContext.globalCompositeOperation=pr[this.data.tt],this.canvasContext.drawImage(e,0,0),this.canvasContext.globalCompositeOperation=`destination-over`,this.canvasContext.drawImage(this.buffers[0],0,0),this.canvasContext.setTransform(this.currentTransform),this.canvasContext.globalCompositeOperation=`source-over`}},renderFrame:function(e){if(!(this.hidden||this.data.hd)&&!(this.data.td===1&&!e)){this.renderTransform(),this.renderRenderable(),this.renderLocalTransform(),this.setBlendMode();var t=this.data.ty===0;this.prepareLayer(),this.globalData.renderer.save(t),this.globalData.renderer.ctxTransform(this.finalTransform.localMat.props),this.globalData.renderer.ctxOpacity(this.finalTransform.localOpacity),this.renderInnerContent(),this.globalData.renderer.restore(t),this.exitLayer(),this.maskManager.hasMasks&&this.globalData.renderer.restore(!0),this._isFirstFrame&&=!1}},destroy:function(){this.canvasContext=null,this.data=null,this.globalData=null,this.maskManager.destroy()},mHelper:new Qe},fr.prototype.hide=fr.prototype.hideElement,fr.prototype.show=fr.prototype.showElement;function mr(e,t,n,r){this.styledShapes=[],this.tr=[0,0,0,0,0,0];var i=4;t.ty===`rc`?i=5:t.ty===`el`?i=6:t.ty===`sr`&&(i=7),this.sh=Ze.getShapeProp(e,t,i,e);var a,o=n.length,s;for(a=0;a=0;--a){if(d=this.searchProcessedElement(e[a]),d?t[a]=n[d-1]:e[a]._shouldRender=r,e[a].ty===`fl`||e[a].ty===`st`||e[a].ty===`gf`||e[a].ty===`gs`)d?t[a].style.closed=!1:t[a]=this.createStyleElement(e[a],m),l.push(t[a].style);else if(e[a].ty===`gr`){if(!d)t[a]=this.createGroupElement(e[a]);else for(c=t[a].it.length,s=0;s=0;--i)t[i].ty===`tr`?(o=n[i].transform,this.renderShapeTransform(e,o)):t[i].ty===`sh`||t[i].ty===`el`||t[i].ty===`rc`||t[i].ty===`sr`?this.renderPath(t[i],n[i]):t[i].ty===`fl`?this.renderFill(t[i],n[i],o):t[i].ty===`st`?this.renderStroke(t[i],n[i],o):t[i].ty===`gf`||t[i].ty===`gs`?this.renderGradientFill(t[i],n[i],o):t[i].ty===`gr`?this.renderShape(o,t[i].it,n[i].it):t[i].ty;r&&this.drawLayer()},hr.prototype.renderStyledShape=function(e,t){if(this._isFirstFrame||t._mdf||e.transforms._mdf){var n=e.trNodes,r=t.paths,i,a,o,s=r._length;n.length=0;var c=e.transforms.finalTransform;for(o=0;o=1?u=.99:u<=-1&&(u=-.99);var d=c*u,f=Math.cos(l+t.a.v)*d+o[0],p=Math.sin(l+t.a.v)*d+o[1];i=a.createRadialGradient(f,p,0,o[0],o[1],c)}var m,h=e.g.p,g=t.g.c,_=1;for(m=0;ma&&c===`xMidYMid slice`||ii&&s===`meet`||ai&&s===`slice`)?this.transformCanvas.tx=(n-this.transformCanvas.w*(r/this.transformCanvas.h))/2*this.renderConfig.dpr:l===`xMax`&&(ai&&s===`slice`)?this.transformCanvas.tx=(n-this.transformCanvas.w*(r/this.transformCanvas.h))*this.renderConfig.dpr:this.transformCanvas.tx=0,u===`YMid`&&(a>i&&s===`meet`||ai&&s===`meet`||a=0;--e)this.elements[e]&&this.elements[e].destroy&&this.elements[e].destroy();this.elements.length=0,this.globalData.canvasContext=null,this.animationItem.container=null,this.destroyed=!0},Q.prototype.renderFrame=function(e,t){if(!(this.renderedFrame===e&&this.renderConfig.clearCanvas===!0&&!t||this.destroyed||e===-1)){this.renderedFrame=e,this.globalData.frameNum=e-this.animationItem._isFirstFrame,this.globalData.frameId+=1,this.globalData._mdf=!this.renderConfig.clearCanvas||t,this.globalData.projectInterface.currentFrame=e;var n,r=this.layers.length;for(this.completeLayers||this.checkLayers(e),n=r-1;n>=0;--n)(this.completeLayers||this.elements[n])&&this.elements[n].prepareFrame(e-this.layers[n].st);if(this.globalData._mdf){for(this.renderConfig.clearCanvas===!0?this.canvasContext.clearRect(0,0,this.transformCanvas.w,this.transformCanvas.h):this.save(),n=r-1;n>=0;--n)(this.completeLayers||this.elements[n])&&this.elements[n].renderFrame();this.renderConfig.clearCanvas!==!0&&this.restore()}}},Q.prototype.buildItem=function(e){var t=this.elements;if(!(t[e]||this.layers[e].ty===99)){var n=this.createItem(this.layers[e],this,this.globalData);t[e]=n,n.initExpressions()}},Q.prototype.checkPendingElements=function(){for(;this.pendingElements.length;)this.pendingElements.pop().checkParenting()},Q.prototype.hide=function(){this.animationItem.container.style.display=`none`},Q.prototype.show=function(){this.animationItem.container.style.display=`block`};function yr(){this.opacity=-1,this.transform=p(`float32`,16),this.fillStyle=``,this.strokeStyle=``,this.lineWidth=``,this.lineCap=``,this.lineJoin=``,this.miterLimit=``,this.id=Math.random()}function br(){this.stack=[],this.cArrPos=0,this.cTr=new Qe;var e,t=15;for(e=0;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].renderFrame()},xr.prototype.destroy=function(){var e;for(e=this.layers.length-1;e>=0;--e)this.elements[e]&&this.elements[e].destroy();this.layers=null,this.elements=null},xr.prototype.createComp=function(e){return new xr(e,this.globalData,this)};function Sr(e,t){this.animationItem=e,this.renderConfig={clearCanvas:t&&t.clearCanvas!==void 0?t.clearCanvas:!0,context:t&&t.context||null,progressiveLoad:t&&t.progressiveLoad||!1,preserveAspectRatio:t&&t.preserveAspectRatio||`xMidYMid meet`,imagePreserveAspectRatio:t&&t.imagePreserveAspectRatio||`xMidYMid slice`,contentVisibility:t&&t.contentVisibility||`visible`,className:t&&t.className||``,id:t&&t.id||``,runExpressions:!t||t.runExpressions===void 0||t.runExpressions},this.renderConfig.dpr=t&&t.dpr||1,this.animationItem.wrapper&&(this.renderConfig.dpr=t&&t.dpr||window.devicePixelRatio||1),this.renderedFrame=-1,this.globalData={frameNum:-1,_mdf:!1,renderConfig:this.renderConfig,currentGlobalAlpha:-1},this.contextData=new br,this.elements=[],this.pendingElements=[],this.transformMat=new Qe,this.completeLayers=!1,this.rendererType=`canvas`,this.renderConfig.clearCanvas&&(this.ctxTransform=this.contextData.transform.bind(this.contextData),this.ctxOpacity=this.contextData.opacity.bind(this.contextData),this.ctxFillStyle=this.contextData.fillStyle.bind(this.contextData),this.ctxStrokeStyle=this.contextData.strokeStyle.bind(this.contextData),this.ctxLineWidth=this.contextData.lineWidth.bind(this.contextData),this.ctxLineCap=this.contextData.lineCap.bind(this.contextData),this.ctxLineJoin=this.contextData.lineJoin.bind(this.contextData),this.ctxMiterLimit=this.contextData.miterLimit.bind(this.contextData),this.ctxFill=this.contextData.fill.bind(this.contextData),this.ctxFillRect=this.contextData.fillRect.bind(this.contextData),this.ctxStroke=this.contextData.stroke.bind(this.contextData),this.save=this.contextData.save.bind(this.contextData))}return u([Q],Sr),Sr.prototype.createComp=function(e){return new xr(e,this.globalData,this)},be(`canvas`,Sr),gt.registerModifier(`tm`,vt),gt.registerModifier(`pb`,yt),gt.registerModifier(`rp`,xt),gt.registerModifier(`rd`,St),gt.registerModifier(`zz`,Pt),gt.registerModifier(`op`,qt),V}))}))(),1);function ur({documentID:e,className:t=``,showError:n=!0}){let r=(0,g.useRef)(null),i=(0,g.useRef)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(null);return(0,g.useEffect)(()=>{let t=!1,n=null;return o(``),c(null),fetch(k.stickerDocumentAnimationURL(e),{credentials:`same-origin`}).then(async e=>{if(!e.ok){let t=await e.json().catch(()=>null);throw Error(t?.error||e.statusText)}if((e.headers.get(`content-type`)??``).includes(`json`)){let n=await e.json();if(t||!r.current)return;i.current?.destroy(),i.current=lr.default.loadAnimation({container:r.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:n});return}let a=await e.blob();t||(n=URL.createObjectURL(a),c(n))}).catch(e=>{t||o(O(e))}),()=>{t=!0,i.current?.destroy(),i.current=null,n&&URL.revokeObjectURL(n)}},[e]),(0,W.jsxs)(`div`,{className:`sticker-doc-cell ${t}`.trim(),children:[s?(0,W.jsx)(`img`,{className:`sticker-doc-image`,src:s,alt:``}):(0,W.jsx)(`div`,{className:`sticker-doc-canvas`,ref:r}),a&&n&&(0,W.jsx)(`span`,{className:`sticker-doc-error`,children:a})]})}function dr({kind:e,onClose:t,onCreated:n}){let r=e===`emoji`?`emoji`:`sticker`,[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(null),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!1),[_,v]=(0,g.useState)(``);async function y(){if(!i.trim()||!o.trim()||!c.trim()||!u){v(`Title, short name, emoji and a first ${r} file are required.`);return}if(!f.trim()){v(`Please enter an operation reason`);return}h(!0),v(``);try{let r=new FormData;r.set(`metadata`,JSON.stringify({command_id:``,reason:f.trim(),confirm:!0,title:i.trim(),short_name:o.trim().toLowerCase(),kind:e,emoji:c.trim()})),r.set(`file`,u,u.name),await k.createStickerSet(r),n(),t()}catch(e){v(O(e))}finally{h(!1)}}return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Create a new ${r} pack`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`New set`}),(0,W.jsx)(`h2`,{children:`Create a new ${r} pack`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:m,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:`Title`}),(0,W.jsx)(`input`,{value:i,maxLength:64,onChange:e=>a(e.target.value)})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:`Short name`}),(0,W.jsx)(`input`,{value:o,maxLength:32,onChange:e=>s(e.target.value),placeholder:`lowercase_short_name`})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:`Emoji`}),(0,W.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:`e.g. 😀`})]})]}),(0,W.jsxs)(`label`,{className:`gift-file-picker ${u?`has-file`:``}`,children:[(0,W.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.webp,application/json,application/x-tgsticker,image/webp`,onChange:e=>d(e.target.files?.[0]??null)}),(0,W.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,W.jsx)(`span`,{className:`gift-field-label`,children:`First ${r}`}),(0,W.jsx)(`strong`,{children:u?u.name:`Choose a TGS, Lottie JSON, or WebP file`})]}),(0,W.jsx)(`span`,{className:`gift-file-action`,children:u?`Change file`:`Choose file`})]}),(0,W.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,W.jsx)(`span`,{children:`Audit reason`}),(0,W.jsx)(`input`,{value:f,placeholder:`Briefly describe why this gift is being imported`,onChange:e=>p(e.target.value)})]}),_&&(0,W.jsx)(K,{children:_})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:m,children:`Close`}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:y,disabled:m,children:[m?(0,W.jsx)(I,{className:`spin`,size:15}):(0,W.jsx)(ot,{size:15}),`Create ${r} pack`]})]})]})}),document.body)}var fr=24;function pr({set:e,onClose:t}){let n=e.Kind===`emoji`?`emoji`:`sticker`,[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(1),l=(0,g.useCallback)(()=>{let t=!1;return o(``),k.stickerSetDocuments(e.ID).then(e=>{t||i(e.document_ids??[])}).catch(e=>{t||o(O(e))}),()=>{t=!0}},[e.ID]);(0,g.useEffect)(()=>(i(null),c(1),l()),[l]);let u=r?.length??0,d=Math.max(1,Math.ceil(u/fr)),f=Math.min(s,d),p=(f-1)*fr,m=r?.slice(p,p+fr)??[],h=m.length===0?0:p+1,_=h===0?0:h+m.length-1;return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal sticker-preview-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e.Title||`#${e.ID}`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`Set contents`}),(0,W.jsx)(`h2`,{children:e.Title||`#${e.ID}`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsx)(mr,{setID:e.ID,noun:n,onAdded:l}),a&&(0,W.jsx)(K,{children:a}),!a&&r===null&&(0,W.jsxs)(`div`,{className:`loading-line`,children:[(0,W.jsx)(I,{className:`spin`,size:18}),` `,`Loading`]}),r!==null&&u===0&&!a&&(0,W.jsx)(`div`,{className:`empty-panel`,children:`This set has no documents.`}),m.length>0&&(0,W.jsx)(`div`,{className:`sticker-doc-grid`,children:m.map(t=>(0,W.jsxs)(`div`,{className:`sticker-doc-grid-cell`,children:[(0,W.jsx)(ur,{documentID:t}),(0,W.jsx)(Z,{compact:!0,tone:`danger`,label:`Remove`,icon:(0,W.jsx)(it,{size:12}),path:`/api/actions/remove-sticker-from-set`,payload:()=>({set_id:e.ID,document_id:t}),onDone:l})]},t))},f),u>fr&&(0,W.jsxs)(`div`,{className:`gift-pager`,children:[(0,W.jsx)(`span`,{className:`gift-pager-range`,children:`Showing ${h}-${_} of ${u}`}),(0,W.jsxs)(`div`,{className:`gift-pager-controls`,children:[(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>c(e=>Math.max(1,e-1)),disabled:f<=1,children:[(0,W.jsx)(_e,{size:14}),` `,`Previous`]}),(0,W.jsx)(`span`,{className:`gift-pager-page`,children:`Page ${f} of ${d}`}),(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>c(e=>Math.min(d,e+1)),disabled:f>=d,children:[`Next`,` `,(0,W.jsx)(ve,{size:14})]})]})]})]})]})}),document.body)}function mr({setID:e,noun:t,onAdded:n}){let[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(!1),[d,f]=(0,g.useState)(``);async function p(){if(!r){f(`Choose a ${t} file first`);return}if(!a.trim()){f(`An emoji is required.`);return}if(!s.trim()){f(`Please enter an operation reason`);return}u(!0),f(``);try{let t=new FormData;t.set(`metadata`,JSON.stringify({command_id:``,reason:s.trim(),confirm:!0,set_id:e,emoji:a.trim()})),t.set(`file`,r,r.name),await k.addStickerToSet(t),i(null),o(``),c(``),n()}catch(e){f(O(e))}finally{u(!1)}}return(0,W.jsxs)(`div`,{className:`sticker-add-form`,children:[(0,W.jsxs)(`label`,{className:`gift-file-picker compact ${r?`has-file`:``}`,children:[(0,W.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.webp,application/json,application/x-tgsticker,image/webp`,onChange:e=>i(e.target.files?.[0]??null)}),(0,W.jsx)(`span`,{className:`gift-file-copy`,children:(0,W.jsx)(`strong`,{children:r?r.name:`Choose a TGS, Lottie JSON, or WebP file`})})]}),(0,W.jsx)(`input`,{className:`small-input`,value:a,onChange:e=>o(e.target.value),placeholder:`e.g. 😀`}),(0,W.jsx)(`input`,{className:`small-input`,value:s,onChange:e=>c(e.target.value),placeholder:`Describe why this operation is being performed`}),(0,W.jsxs)(`button`,{className:`btn primary compact-btn`,type:`button`,onClick:p,disabled:l,children:[l?(0,W.jsx)(I,{className:`spin`,size:14}):(0,W.jsx)(We,{size:14}),` `,`Add ${t}`]}),d&&(0,W.jsx)(`span`,{className:`sticker-add-form-error`,children:d})]})}function hr({kind:e}){let[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(10),[d,f]=(0,g.useState)(1),[p,m]=(0,g.useState)({}),[h,_]=(0,g.useState)({}),[v,y]=(0,g.useState)(null),[b,x]=(0,g.useState)(!1),S=e===`emoji`?`Emoji`:`Stickers`,C=e===`emoji`?`Custom-emoji packs — system packs aren't shown here, they're not hand-edited`:`Sticker packs — system packs (dice, animated emoji, gifts) aren't shown here, they're not hand-edited`,w=e===`emoji`?`emoji`:`sticker`;async function T(){o(!0),c(``);try{n((await k.stickerSets(e)).rows??[])}catch(e){c(O(e))}finally{o(!1)}}(0,g.useEffect)(()=>{T()},[e]);let E=(0,g.useMemo)(()=>{let e=r.trim().toLowerCase();return e?t.filter(t=>String(t.ID).includes(e)||t.ShortName.toLowerCase().includes(e)||t.Title.toLowerCase().includes(e)):t},[t,r]);(0,g.useEffect)(()=>{f(1)},[r,l,e]);let D=l===`all`?1:Math.max(1,Math.ceil(E.length/l)),A=Math.min(d,D),j=(0,g.useMemo)(()=>{if(l===`all`)return E;let e=(A-1)*l;return E.slice(e,e+l)},[E,A,l]),M=j.length===0?0:l===`all`?1:(A-1)*l+1,N=M===0?0:M+j.length-1,P=(0,g.useMemo)(()=>({total:t.length,official:t.filter(e=>e.Official).length,archived:t.filter(e=>e.Archived).length}),[t]);return(0,W.jsxs)(Dt,{title:S,eyebrow:C,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>T(),disabled:a,children:[(0,W.jsx)(qe,{size:15}),` `,`Refresh`]}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:()=>x(!0),children:[(0,W.jsx)(We,{size:15}),` `,`Create ${w} pack`]})]}),children:[s&&(0,W.jsx)(K,{children:s}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Total sets`,value:String(P.total)}),(0,W.jsx)(J,{label:`Official`,value:String(P.official),tone:`good`}),(0,W.jsx)(J,{label:`Archived`,value:String(P.archived),tone:P.archived>0?`warn`:`neutral`})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`div`,{className:`toolbar`,children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`Search set ID, short name or title`})]}),(0,W.jsxs)(`label`,{className:`gift-page-size`,children:[(0,W.jsx)(`span`,{children:`Per page`}),(0,W.jsxs)(`select`,{value:String(l),onChange:e=>u(e.target.value===`all`?`all`:Number(e.target.value)),children:[(0,W.jsx)(`option`,{value:`10`,children:`10`}),(0,W.jsx)(`option`,{value:`20`,children:`20`}),(0,W.jsx)(`option`,{value:`50`,children:`50`}),(0,W.jsx)(`option`,{value:`100`,children:`100`}),(0,W.jsx)(`option`,{value:`all`,children:`All`})]})]}),(0,W.jsx)(`span`,{className:`gift-list-summary`,children:`Showing ${E.length} of ${t.length}`})]})}),(0,W.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Logo`}),(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Short name`}),(0,W.jsx)(`th`,{children:`Title`}),(0,W.jsx)(`th`,{children:`Documents`}),(0,W.jsx)(`th`,{children:`Official`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Sort order`}),(0,W.jsx)(`th`,{children:`Actions`})]})}),(0,W.jsxs)(`tbody`,{children:[j.map(e=>(0,W.jsxs)(`tr`,{className:e.Archived?`gift-row-disabled`:``,children:[(0,W.jsx)(`td`,{children:e.CoverDocumentID?(0,W.jsx)(ur,{documentID:e.CoverDocumentID,className:`list-thumb`,showError:!1}):(0,W.jsx)(`div`,{className:`sticker-list-thumb-empty`,children:(0,W.jsx)(Ae,{size:14})})}),(0,W.jsx)(`td`,{className:`mono`,children:e.ID}),(0,W.jsx)(`td`,{className:`mono`,children:e.ShortName||(0,W.jsx)(`span`,{className:`muted-cell`,children:`None`})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,W.jsx)(`input`,{className:`small-input title-input`,value:h[e.ID]??e.Title,onChange:t=>_(n=>({...n,[e.ID]:t.target.value}))}),(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/rename-sticker-set`,payload:()=>({set_id:e.ID,title:(h[e.ID]??e.Title).trim()}),onDone:()=>void T()})]})}),(0,W.jsx)(`td`,{children:e.Count}),(0,W.jsx)(`td`,{children:e.Official?(0,W.jsx)(q,{tone:`good`,children:`Yes`}):(0,W.jsx)(q,{children:`No`})}),(0,W.jsx)(`td`,{children:e.Archived?(0,W.jsx)(q,{tone:`danger`,children:`Archived`}):(0,W.jsx)(q,{tone:`good`,children:`Enabled`})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,W.jsx)(`input`,{type:`number`,className:`small-input`,value:p[e.ID]??String(e.SortOrder),onChange:t=>m(n=>({...n,[e.ID]:t.target.value}))}),(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/set-sticker-set-sort-order`,payload:()=>({set_id:e.ID,sort_order:Number(p[e.ID]??e.SortOrder)}),onDone:()=>void T()})]})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>y(e),children:[(0,W.jsx)(Ce,{size:13}),` `,`View`]}),(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:e.Archived?`Unarchive`:`Archive`,path:`/api/actions/set-sticker-set-archived`,payload:()=>({set_id:e.ID,archived:!e.Archived}),onDone:()=>void T()}),(0,W.jsx)(Z,{compact:!0,tone:`danger`,label:`Delete`,path:`/api/actions/delete-sticker-set`,payload:()=>({set_id:e.ID}),onDone:()=>void T()})]})})]},e.ID)),j.length===0&&(0,W.jsx)(jt,{colSpan:9})]})]})}),l!==`all`&&E.length>0&&(0,W.jsxs)(`div`,{className:`gift-pager`,children:[(0,W.jsx)(`span`,{className:`gift-pager-range`,children:`Showing ${M}-${N} of ${E.length}`}),(0,W.jsxs)(`div`,{className:`gift-pager-controls`,children:[(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>f(e=>Math.max(1,e-1)),disabled:A<=1,children:[(0,W.jsx)(_e,{size:14}),` `,`Previous`]}),(0,W.jsx)(`span`,{className:`gift-pager-page`,children:`Page ${A} of ${D}`}),(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>f(e=>Math.min(D,e+1)),disabled:A>=D,children:[`Next`,` `,(0,W.jsx)(ve,{size:14})]})]})]}),v&&(0,W.jsx)(pr,{set:v,onClose:()=>y(null)}),b&&(0,W.jsx)(dr,{kind:e,onClose:()=>x(!1),onCreated:()=>void T()})]})}var gr=[`Love`,`Approval`,`Disapproval`,`Cheers`,`Laughter`,`Astonishment`,`Sadness`,`Anger`,`Neutral`,`Doubt`,`Silly`];function _r({documentID:e}){let[t,n]=(0,g.useState)(!1);return t?(0,W.jsx)(`div`,{className:`sticker-list-thumb-empty`,children:(0,W.jsx)(Ae,{size:14})}):(0,W.jsx)(`video`,{className:`gif-catalog-thumb`,src:k.gifCatalogDocumentPreviewURL(e),muted:!0,loop:!0,autoPlay:!0,playsInline:!0,onError:()=>n(!0)})}function vr(){let[e,t]=(0,g.useState)([]),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(!1),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(10),[u,d]=(0,g.useState)(1),[f,p]=(0,g.useState)({}),[m,h]=(0,g.useState)({}),[_,v]=(0,g.useState)(!1);async function y(){a(!0),s(``);try{t((await k.gifCatalog()).rows??[])}catch(e){s(O(e))}finally{a(!1)}}(0,g.useEffect)(()=>{y()},[]);let b=(0,g.useMemo)(()=>{let t=n.trim().toLowerCase();return t?e.filter(e=>e.ID.includes(t)||e.Title.toLowerCase().includes(t)):e},[e,n]);(0,g.useEffect)(()=>{d(1)},[n,c]);let x=c===`all`?1:Math.max(1,Math.ceil(b.length/c)),S=Math.min(u,x),C=(0,g.useMemo)(()=>{if(c===`all`)return b;let e=(S-1)*c;return b.slice(e,e+c)},[b,S,c]),w=C.length===0?0:c===`all`?1:(S-1)*c+1,T=w===0?0:w+C.length-1,E=(0,g.useMemo)(()=>({total:e.length,enabled:e.filter(e=>e.Enabled).length,uncategorized:e.filter(e=>!e.Category).length}),[e]);return(0,W.jsxs)(Dt,{title:`GIFs`,eyebrow:`Curated GIFs served by @gif in the client's GIF picker (trending + search)`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>y(),disabled:i,children:[(0,W.jsx)(qe,{size:15}),` `,`Refresh`]}),(0,W.jsx)(Z,{tone:`neutral`,label:`Auto-categorize`,path:`/api/actions/auto-categorize-gif-catalog`,payload:()=>({}),onDone:()=>void y()}),(0,W.jsx)(Z,{tone:`danger`,label:`Delete uncategorized`,path:`/api/actions/delete-uncategorized-gifs`,payload:()=>({}),onDone:()=>void y()}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:()=>v(!0),children:[(0,W.jsx)(We,{size:15}),` `,`Add GIF`]})]}),children:[o&&(0,W.jsx)(K,{children:o}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Total GIFs`,value:String(E.total)}),(0,W.jsx)(J,{label:`Enabled`,value:String(E.enabled),tone:`good`}),(0,W.jsx)(J,{label:`Uncategorized`,value:String(E.uncategorized),tone:E.uncategorized>0?`warn`:void 0})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`div`,{className:`toolbar`,children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:`Search ID or title`})]}),(0,W.jsxs)(`label`,{className:`gift-page-size`,children:[(0,W.jsx)(`span`,{children:`Per page`}),(0,W.jsxs)(`select`,{value:String(c),onChange:e=>l(e.target.value===`all`?`all`:Number(e.target.value)),children:[(0,W.jsx)(`option`,{value:`10`,children:`10`}),(0,W.jsx)(`option`,{value:`20`,children:`20`}),(0,W.jsx)(`option`,{value:`50`,children:`50`}),(0,W.jsx)(`option`,{value:`100`,children:`100`}),(0,W.jsx)(`option`,{value:`all`,children:`All`})]})]}),(0,W.jsx)(`span`,{className:`gift-list-summary`,children:`Showing ${b.length} of ${e.length}`})]})}),(0,W.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Preview`}),(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Title`}),(0,W.jsx)(`th`,{children:`Document ID`}),(0,W.jsx)(`th`,{children:`Added by`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Category`}),(0,W.jsx)(`th`,{children:`Sort order`}),(0,W.jsx)(`th`,{children:`Actions`})]})}),(0,W.jsxs)(`tbody`,{children:[C.map(e=>(0,W.jsxs)(`tr`,{className:e.Enabled?``:`gift-row-disabled`,children:[(0,W.jsx)(`td`,{children:(0,W.jsx)(_r,{documentID:e.DocumentID})}),(0,W.jsx)(`td`,{className:`mono`,children:e.ID}),(0,W.jsx)(`td`,{children:e.Title||(0,W.jsx)(`span`,{className:`muted-cell`,children:`Untitled`})}),(0,W.jsx)(`td`,{className:`mono`,children:e.DocumentID}),(0,W.jsx)(`td`,{children:e.CreatedBy||(0,W.jsx)(`span`,{className:`muted-cell`,children:`—`})}),(0,W.jsx)(`td`,{children:e.Enabled?(0,W.jsx)(q,{tone:`good`,children:`Enabled`}):(0,W.jsx)(q,{tone:`danger`,children:`Disabled`})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,W.jsxs)(`select`,{className:`small-input`,value:m[e.ID]??e.Category,onChange:t=>h(n=>({...n,[e.ID]:t.target.value})),children:[(0,W.jsx)(`option`,{value:``,children:`Uncategorized`}),gr.map(e=>(0,W.jsx)(`option`,{value:e,children:e},e))]}),(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/set-gif-catalog-category`,payload:()=>({id:e.ID,category:m[e.ID]??e.Category}),onDone:()=>void y()})]})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,W.jsx)(`input`,{type:`number`,className:`small-input`,value:f[e.ID]??String(e.SortOrder),onChange:t=>p(n=>({...n,[e.ID]:t.target.value}))}),(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/set-gif-catalog-sort-order`,payload:()=>({id:e.ID,sort_order:Number(f[e.ID]??e.SortOrder)}),onDone:()=>void y()})]})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:e.Enabled?`Disable`:`Enable`,path:`/api/actions/set-gif-catalog-enabled`,payload:()=>({id:e.ID,enabled:!e.Enabled}),onDone:()=>void y()}),(0,W.jsx)(Z,{compact:!0,tone:`danger`,label:`Delete`,path:`/api/actions/delete-gif-catalog-entry`,payload:()=>({id:e.ID}),onDone:()=>void y()})]})})]},e.ID)),C.length===0&&(0,W.jsx)(jt,{colSpan:9})]})]})}),c!==`all`&&b.length>0&&(0,W.jsxs)(`div`,{className:`gift-pager`,children:[(0,W.jsx)(`span`,{className:`gift-pager-range`,children:`Showing ${w}-${T} of ${b.length}`}),(0,W.jsxs)(`div`,{className:`gift-pager-controls`,children:[(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>d(e=>Math.max(1,e-1)),disabled:S<=1,children:[(0,W.jsx)(_e,{size:14}),` `,`Previous`]}),(0,W.jsx)(`span`,{className:`gift-pager-page`,children:`Page ${S} of ${x}`}),(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>d(e=>Math.min(x,e+1)),disabled:S>=x,children:[`Next`,` `,(0,W.jsx)(ve,{size:14})]})]})]}),_&&(0,W.jsx)(Q,{onClose:()=>v(!1),onCreated:()=>void y()})]})}function Q({onClose:e,onCreated:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``);function m(e){a(e),s(t=>(t&&URL.revokeObjectURL(t),e?URL.createObjectURL(e):null))}async function h(){if(!n.trim()||!i){p(`Title and a GIF/MP4 file are required.`);return}if(!c.trim()){p(`Please enter an operation reason`);return}d(!0),p(``);try{let r=new FormData;r.set(`metadata`,JSON.stringify({command_id:``,reason:c.trim(),confirm:!0,title:n.trim()})),r.set(`file`,i,i.name),await k.createGifCatalogEntry(r),t(),e()}catch(e){p(O(e))}finally{d(!1)}}return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Add a GIF`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`New catalog entry`}),(0,W.jsx)(`h2`,{children:`Add a GIF`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,disabled:u,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsx)(`div`,{className:`gift-fields-grid`,children:(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:`Title`}),(0,W.jsx)(`input`,{value:n,maxLength:128,onChange:e=>r(e.target.value)})]})}),(0,W.jsxs)(`label`,{className:`gift-file-picker ${i?`has-file`:``}`,children:[(0,W.jsx)(`input`,{type:`file`,accept:`.gif,.mp4,image/gif,video/mp4`,onChange:e=>m(e.target.files?.[0]??null)}),(0,W.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,W.jsx)(`span`,{className:`gift-field-label`,children:`File`}),(0,W.jsx)(`strong`,{children:i?i.name:`Choose a GIF or MP4 file`})]}),(0,W.jsx)(`span`,{className:`gift-file-action`,children:i?`Change file`:`Choose file`})]}),o&&(0,W.jsx)(`div`,{className:`gif-catalog-preview`,children:i?.type===`video/mp4`?(0,W.jsx)(`video`,{src:o,autoPlay:!0,loop:!0,muted:!0,playsInline:!0}):(0,W.jsx)(`img`,{src:o,alt:``})}),(0,W.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,W.jsx)(`span`,{children:`Audit reason`}),(0,W.jsx)(`input`,{value:c,placeholder:`Briefly describe why this GIF is being added`,onChange:e=>l(e.target.value)})]}),f&&(0,W.jsx)(K,{children:f})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,disabled:u,children:`Close`}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:h,disabled:u,children:[u?(0,W.jsx)(I,{className:`spin`,size:15}):(0,W.jsx)(ot,{size:15}),`Add GIF`]})]})]})}),document.body)}var yr=`open,in_review,action_pending,action_failed,appeal_review`,br=[{value:yr,label:`Active queue`},{value:`open,in_review,action_pending,action_failed,resolved,dismissed,appeal_review`,label:`All statuses`},{value:`open`,label:`Open`},{value:`in_review`,label:`In review`},{value:`action_pending`,label:`Action pending`},{value:`action_failed`,label:`Action failed`},{value:`appeal_review`,label:`Appeal review`},{value:`resolved`,label:`Resolved`},{value:`dismissed`,label:`Dismissed`}];function xr({navigate:e}){let[t,n]=(0,g.useState)(yr),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);try{let e=new URLSearchParams({statuses:t,limit:`100`});r.trim()&&e.set(`assigned_to`,r.trim()),o((await k.moderationCases(e)).cases)}catch(e){u(O(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d()},[]);let f=a.filter(e=>e.Status===`action_pending`||e.Status===`action_failed`).length,p=a.filter(e=>e.Severity===4).length;return(0,W.jsxs)(Dt,{title:`Reports and Moderation`,eyebrow:`Moderation / Cases`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:d,disabled:s,children:[(0,W.jsx)(qe,{size:15,className:s?`spin`:``}),` `,`Refresh`]}),children:[l&&(0,W.jsx)(K,{children:l}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Current queue`,value:String(a.length)}),(0,W.jsx)(J,{label:`Critical cases`,value:String(p),tone:p?`danger`:`neutral`}),(0,W.jsx)(J,{label:`Pending / failed actions`,value:String(f),tone:f?`warn`:`good`})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),d()},children:[(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Status`}),(0,W.jsx)(`select`,{"aria-label":`Case status filter`,value:t,onChange:e=>n(e.target.value),children:br.map(e=>(0,W.jsx)(`option`,{value:e.value,children:e.label},e.value))})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Reviewer`}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`Leave blank for all`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:s,children:[(0,W.jsx)(Ze,{size:15}),` `,`Search`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Case`}),(0,W.jsx)(`th`,{children:`Target`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Severity`}),(0,W.jsx)(`th`,{children:`Reports / Reporters`}),(0,W.jsx)(`th`,{children:`Reviewer`}),(0,W.jsx)(`th`,{children:`Latest report`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[a.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsxs)(`td`,{className:`mono`,children:[`#`,t.ID]}),(0,W.jsx)(`td`,{className:`mono`,children:Dr(t.Target.Type,t.Target.ID)}),(0,W.jsx)(`td`,{children:(0,W.jsx)(Sr,{status:t.Status})}),(0,W.jsx)(`td`,{children:(0,W.jsx)(wr,{value:t.Severity})}),(0,W.jsxs)(`td`,{children:[t.ReportCount,` / `,t.DistinctReporterCount]}),(0,W.jsx)(`td`,{children:t.AssignedTo||`-`}),(0,W.jsx)(`td`,{children:U(t.LastReportAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/moderation/${t.ID}`),children:[`Review`,` `,(0,W.jsx)(ve,{size:14})]})})]},t.ID)),a.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})})]})}function Sr({status:e}){return(0,W.jsx)(q,{tone:e===`resolved`||e===`dismissed`?`good`:e===`action_failed`?`danger`:e===`action_pending`?`warn`:`neutral`,children:Er(`status`,e)})}var Cr={low:`Low`,medium:`Medium`,high:`High`,critical:`Critical`};function wr({value:e}){let t=[``,`low`,`medium`,`high`,`critical`][e];return(0,W.jsx)(q,{tone:e>=4?`danger`:e>=3?`warn`:`neutral`,children:t?Cr[t]:e})}var Tr={status:{open:`Open`,in_review:`In review`,action_pending:`Action pending`,action_failed:`Action failed`,appeal_review:`Appeal review`,resolved:`Resolved`,dismissed:`Dismissed`},targetType:{channel:`Channel`,chat:`Group`,user:`Account`},source:{account_peer:`Account / peer`,antispam_false_positive:`Anti-spam false positive`,channel_spam:`Channel spam`,encrypted_spam:`Encrypted-chat spam`,ephemeral:`Ephemeral media`,messages:`Messages`,messages_spam:`Message spam`,profile_photo:`Profile photo`,reaction:`Reaction`,sponsored:`Sponsored message`,story:`Story`},reason:{child_abuse:`Child abuse`,copyright:`Copyright`,fake:`Fake`,geo_irrelevant:`Location-irrelevant`,illegal_drugs:`Illegal drugs`,other:`Other`,personal_details:`Personal details`,pornography:`Pornography`,spam:`Spam`,violence:`Violence`}};function Er(e,t){return Tr[e]?.[t]??t}function Dr(e,t){return`${Er(`targetType`,e)} #${t}`}function Or({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(`no_violation`),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!0),[_,v]=(0,g.useState)(!1),[y,b]=(0,g.useState)(``);function x(e){a(e),e&&(d(e.Items.filter(e=>e.Kind===`message`).map(e=>Number(e.ItemID)).filter(e=>Number.isSafeInteger(e)&&e>0).join(`, `)),p(String(e.ReporterUserID)))}async function S(){b(``);try{let t=await k.moderationCase(e);r(t);let n=t.ReportIDs[0];x(n?await k.moderationReport(n):null)}catch(e){b(O(e))}}(0,g.useEffect)(()=>{S()},[e]);let C=(0,g.useMemo)(()=>kr(c,n?.Case.Target.Type,Ar(u),Number(f),m),[c,n?.Case.Target.Type,u,f,m]),w=(0,g.useMemo)(()=>n?jr(n):{actions:[],label:`None`,blocked:!1},[n]);async function T(){if(n){v(!0),b(``);try{await k.claimModerationCase(e,n.Case.Version),await S()}catch(e){b(O(e))}finally{v(!1)}}}async function E(){if(!n||!o.trim()){b(`A review reason is required.`);return}if(c===`delete_messages`&&C.length===0){b(n.Case.Target.Type===`user`?`Private-message deletion requires valid evidence message IDs and the reporter's owner_user_id.`:`Channel-message deletion requires at least one valid evidence message ID.`);return}if(window.confirm(`Submit the “${Mr(c)}” decision? The action will run through the durable action queue.`)){v(!0),b(``);try{r((await k.decideModerationCase(e,{expected_version:n.Case.Version,reason:o.trim(),kind:c===`no_violation`?`no_violation`:`violation`,actions:C})).case),s(``)}catch(e){b(O(e))}finally{v(!1)}}}async function D(t,i){if(!n||!o.trim()){b(`An appeal review reason is required.`);return}if(window.confirm(i?`Grant this appeal?`:`Deny this appeal?`)){v(!0);try{r((await k.reviewModerationAppeal(e,t,{expected_version:n.Case.Version,reason:o.trim(),granted:i,actions:i?w.actions:[]})).case),s(``)}catch(e){b(O(e))}finally{v(!1)}}}if(y&&!n)return(0,W.jsx)(K,{children:y});if(!n)return(0,W.jsx)(X,{label:`Loading moderation case…`});let A=n.Case,j=A.Status===`open`||A.Status===`in_review`||A.Status===`appeal_review`,M=(A.Status===`in_review`||A.Status===`action_failed`)&&!!A.AssignedTo,N=M&&(A.Status!==`action_failed`||c!==`no_violation`),P=n.Appeals.find(e=>e.Status===`pending`);return(0,W.jsxs)(Dt,{title:`Review case #${A.ID}`,eyebrow:`Moderation / Case detail`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/moderation`),children:[(0,W.jsx)(ue,{size:15}),` `,`Back to queue`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:S,children:[(0,W.jsx)(qe,{size:15}),` `,`Refresh`]})]}),children:[y&&(0,W.jsx)(K,{children:y}),(0,W.jsx)(kt,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:Dr(A.Target.Type,A.Target.ID)}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`Version ${A.Version} · Updated ${U(A.UpdatedAt)}`})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(Sr,{status:A.Status}),(0,W.jsx)(wr,{value:A.Severity})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Target`,value:Dr(A.Target.Type,A.Target.ID),mono:!0}),(0,W.jsx)(Y,{label:`Reports`,value:`${A.ReportCount} reports from ${A.DistinctReporterCount} reporters`}),(0,W.jsx)(Y,{label:`Reviewer`,value:A.AssignedTo||`-`}),(0,W.jsx)(Y,{label:`First / latest report`,value:`${U(A.FirstReportAt)} / ${U(A.LastReportAt)}`})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Report evidence`,text:`Shows up to the latest 100 reports; snapshots are frozen when reports are admitted.`}),(0,W.jsx)(`div`,{className:`toolbar`,children:n.ReportIDs.map(e=>(0,W.jsxs)(`button`,{className:`btn`,onClick:async()=>x(await k.moderationReport(e)),children:[`#`,e]},e))}),i&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Source / Reason`,value:`${Er(`source`,i.Source)} / ${Er(`reason`,i.Reason)}`}),(0,W.jsx)(Y,{label:`Reporter`,value:String(i.ReporterUserID),mono:!0}),(0,W.jsx)(Y,{label:`Option`,value:i.Option,mono:!0}),(0,W.jsx)(Y,{label:`Time`,value:U(i.CreatedAt)})]}),i.Comment&&(0,W.jsx)(`p`,{className:`about-text`,children:i.Comment}),(0,W.jsx)(Mt,{value:JSON.stringify(i,null,2)})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Decision and action audit`,text:`Actions run idempotently through a lease worker; failures retain their error and attempt count.`}),(0,W.jsx)(Mt,{value:JSON.stringify({decisions:n.Decisions,actions:n.Actions},null,2)})]}),n.Appeals.length>0&&(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Appeals`}),(0,W.jsx)(Mt,{value:JSON.stringify(n.Appeals,null,2)})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:`Case actions`}),j&&(0,W.jsxs)(`button`,{className:`btn primary icon-text`,disabled:_,onClick:T,children:[(0,W.jsx)(Qe,{size:15}),` `,A.AssignedTo?`Renew claim`:`Claim case`]}),(0,W.jsxs)(`label`,{className:`field`,children:[(0,W.jsx)(`span`,{children:`Review reason`}),(0,W.jsx)(`textarea`,{value:o,onChange:e=>s(e.target.value),rows:5})]}),(0,W.jsxs)(`label`,{className:`field`,children:[(0,W.jsx)(`span`,{children:`Decision template`}),(0,W.jsxs)(`select`,{value:c,onChange:e=>l(e.target.value),children:[(0,W.jsx)(`option`,{value:`no_violation`,children:`No violation (dismiss report)`}),(0,W.jsx)(`option`,{value:`scam`,children:`Mark as SCAM`}),(0,W.jsx)(`option`,{value:`fake`,children:`Mark as FAKE`}),(0,W.jsx)(`option`,{value:`freeze`,children:`Freeze account`}),(0,W.jsx)(`option`,{value:`scam_freeze`,children:`SCAM + freeze`}),(0,W.jsx)(`option`,{value:`fake_freeze`,children:`FAKE + freeze`}),(0,W.jsx)(`option`,{value:`delete_messages`,children:`Delete messages covered by evidence`}),(0,W.jsx)(`option`,{value:`delete_account`,children:`Delete account`})]})]}),c===`delete_messages`&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`label`,{className:`field`,children:[(0,W.jsx)(`span`,{children:`Evidence message IDs (comma-separated)`}),(0,W.jsx)(`input`,{value:u,onChange:e=>d(e.target.value),placeholder:`101, 102`})]}),A.Target.Type===`user`&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`label`,{className:`field`,children:[(0,W.jsx)(`span`,{children:`Private-chat owner_user_id`}),(0,W.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),inputMode:`numeric`})]}),(0,W.jsxs)(`label`,{className:`field checkbox-field`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),(0,W.jsx)(`span`,{children:`Revoke for both sides`})]})]}),(0,W.jsx)(K,{children:`The server will verify again that every message ID exists in this case's immutable report evidence.`})]}),A.Status===`action_failed`&&c===`no_violation`&&(0,W.jsx)(K,{children:`The action was partially executed and cannot be changed directly to no violation. Select a new action to retry while retaining the previous failure audit.`}),M&&(0,W.jsxs)(`button`,{className:`btn danger icon-text`,disabled:_||!N,onClick:E,children:[(0,W.jsx)(F,{size:15}),` `,A.Status===`action_failed`?`Retry action`:`Submit decision`]}),P&&A.AssignedTo&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsx)(`div`,{className:`dock-title`,children:`Appeal review #${P.ID}`}),(0,W.jsx)(Y,{label:`Automatic remedy after approval`,value:w.label}),w.blocked&&(0,W.jsx)(K,{children:`The case contains a completed irreversible deletion. It cannot be marked as approved and restored; deny it or escalate for manual handling.`}),(0,W.jsx)(`button`,{className:`btn`,disabled:_,onClick:()=>D(P.ID,!1),children:`Deny appeal`}),(0,W.jsx)(`button`,{className:`btn primary`,disabled:_||w.blocked,onClick:()=>D(P.ID,!0),children:`Grant appeal`})]})]})})]})}function kr(e,t,n,r,i){switch(e){case`scam`:return[{kind:`mark_scam`,payload:{}}];case`fake`:return[{kind:`mark_fake`,payload:{}}];case`freeze`:return[{kind:`freeze_account`,payload:{}}];case`scam_freeze`:return[{kind:`mark_scam`,payload:{}},{kind:`freeze_account`,payload:{}}];case`fake_freeze`:return[{kind:`mark_fake`,payload:{}},{kind:`freeze_account`,payload:{}}];case`delete_messages`:return n.length===0?[]:t===`channel`?[{kind:`delete_channel_message`,payload:{ids:n}}]:t===`user`&&Number.isSafeInteger(r)&&r>0?[{kind:`delete_private_message`,payload:{owner_user_id:r,ids:n,revoke:i}}]:[];case`delete_account`:return[{kind:`delete_account`,payload:{}}];default:return[]}}function Ar(e){let t=e.split(/[,\s]+/).filter(Boolean).map(Number);return t.length===0||t.some(e=>!Number.isSafeInteger(e)||e<=0)?[]:[...new Set(t)]}function jr(e){let t=!1,n=!1,r=!1;for(let i of[...e.Actions].sort((e,t)=>e.ID-t.ID))if(i.Status===`succeeded`)switch(i.Kind){case`mark_scam`:case`mark_fake`:t=!0;break;case`clear_peer_flags`:t=!1;break;case`freeze_account`:n=!0;break;case`unfreeze_account`:n=!1;break;case`delete_private_message`:case`delete_channel_message`:case`delete_account`:r=!0;break}let i=[],a=[];return t&&(i.push({kind:`clear_peer_flags`,payload:{}}),a.push(`Clear SCAM / FAKE`)),n&&(i.push({kind:`unfreeze_account`,payload:{}}),a.push(`Unfreeze account`)),{actions:i,label:a.join(` + `)||`No recovery action needed`,blocked:r}}function Mr(e){return{no_violation:`No violation (dismiss report)`,scam:`Mark as SCAM`,fake:`Mark as FAKE`,freeze:`Freeze account`,scam_freeze:`SCAM + freeze`,fake_freeze:`FAKE + freeze`,delete_messages:`Delete messages covered by evidence`,delete_account:`Delete account`}[e]}function Nr({navigate:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)([]),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(0),[l,u]=(0,g.useState)(!1),[d,f]=(0,g.useState)(``);async function p(){try{n(await k.storageStats())}catch{}}async function m(e=!1){u(!0),f(``);let t=new URLSearchParams({limit:`50`,offset:String(e?s:0)});try{let n=await k.storageAccounts(t),r=n.rows??[];i(t=>e?[...t,...r]:r),c(n.next_offset),o(!!n.has_more)}catch(e){f(O(e))}finally{u(!1)}}function h(){p(),m(!1)}(0,g.useEffect)(()=>{h()},[]);let _=t?Math.max(0,Number(t.LogicalBytes)-Number(t.PhysicalBytes)):0;return(0,W.jsxs)(Dt,{title:`Storage`,eyebrow:`Media / Storage usage`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:h,disabled:l,children:[(0,W.jsx)(qe,{size:15,className:l?`spin`:``}),` `,`Refresh`]}),children:[d&&(0,W.jsx)(K,{children:d}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Physical usage (on disk / S3)`,value:t?wt(t.PhysicalBytes):`-`}),(0,W.jsx)(J,{label:`Logical usage (sum per account)`,value:t?wt(t.LogicalBytes):`-`}),(0,W.jsx)(J,{label:`Saved by dedup`,value:wt(String(_)),tone:_>0?`good`:`neutral`}),(0,W.jsx)(J,{label:`Backend`,value:t?.BackendKind??`-`})]}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Documents`,value:t?_t(t.DocumentCount):`-`}),(0,W.jsx)(J,{label:`Photos`,value:t?_t(t.PhotoCount):`-`}),(0,W.jsx)(J,{label:`Accounts with media`,value:t?_t(t.AccountCount):`-`}),(0,W.jsx)(J,{label:`Unattributed`,value:t?wt(t.UnattributedBytes):`-`,tone:t&&Number(t.UnattributedBytes)>0?`warn`:`neutral`})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`User ID`}),(0,W.jsx)(`th`,{children:`Account`}),(0,W.jsx)(`th`,{children:`Storage used`}),(0,W.jsx)(`th`,{children:`Files`})]})}),(0,W.jsxs)(`tbody`,{children:[r.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:e.UserID}),(0,W.jsx)(`td`,{children:H(e.Username)||e.FirstName||`-`}),(0,W.jsx)(`td`,{className:`mono`,children:wt(e.Bytes)}),(0,W.jsx)(`td`,{className:`mono`,children:_t(e.FileCount)})]},e.UserID)),r.length===0&&(0,W.jsx)(jt,{colSpan:4})]})]})}),a&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:l,children:[l?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(ge,{size:15}),` `,`Load more`]})})]})}function Pr({loader:e,cacheKey:t,className:n,playOnHover:r=!0,onError:i}){let a=(0,g.useRef)(null),o=(0,g.useRef)(null);(0,g.useEffect)(()=>{let t=!1;return e().then(e=>{t||!a.current||(o.current?.destroy(),o.current=lr.default.loadAnimation({container:a.current,renderer:`canvas`,loop:!0,autoplay:!1,animationData:structuredClone(e)}),o.current.goToAndStop(0,!0))}).catch(()=>i?.()),()=>{t=!0,o.current?.destroy(),o.current=null}},[t]);function s(){r&&o.current?.play()}function c(){r&&o.current?.goToAndStop(0,!0)}return(0,W.jsx)(`div`,{className:n,ref:a,onMouseEnter:s,onMouseLeave:c})}function Fr(e){let t=e.toLowerCase();return t.includes(`tgsticker`)||t.includes(`lottie`)||t.includes(`json`)}function Ir({row:e}){let[t,n]=(0,g.useState)(!Fr(e.MimeType));return(0,g.useEffect)(()=>{n(!Fr(e.MimeType))},[e.DocumentID,e.MimeType]),t?(0,W.jsx)(`div`,{className:`emoji-picker-glyph`,children:e.Alt||`🙂`}):(0,W.jsx)(Pr,{className:`emoji-picker-anim`,cacheKey:e.DocumentID,loader:()=>k.emojiAnimation(e.DocumentID),onError:()=>n(!0)})}function Lr({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``),d=a.find(e=>e.DocumentID===t)??null;async function f(){c(!0),u(``);let e=new URLSearchParams({limit:`24`});r.trim()&&e.set(`q`,r.trim());try{o((await k.emoji(e)).rows??[])}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(``),children:[(0,W.jsx)(ut,{size:13}),` `,`Clear`]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(he,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:d?.Alt||`—`}),(0,W.jsx)(`span`,{className:`mono`,children:t})]}),(0,W.jsx)(`span`,{children:d?.SetTitle||`-`})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:`Search document ID or emoji`}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:s,children:s?(0,W.jsx)(I,{size:14,className:`spin`}):`Search`})]}),l&&(0,W.jsx)(`div`,{className:`picker-error`,children:l}),(0,W.jsxs)(`div`,{className:`picker-results emoji-picker-results`,children:[a.map(e=>(0,W.jsxs)(`button`,{className:`picker-row emoji-picker-row ${t===e.DocumentID?`selected`:``}`,type:`button`,onClick:()=>n(e.DocumentID),children:[(0,W.jsx)(Ir,{row:e}),(0,W.jsx)(`span`,{className:`mono`,children:e.DocumentID}),(0,W.jsx)(`span`,{children:e.SetTitle||`—`})]},e.DocumentID)),a.length===0&&!s?(0,W.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}var Rr=[`pending`,`approved`,`rejected`,`revoked`],zr=[`user`,`channel`],Br={pending:`Pending`,approved:`Approved`,rejected:`Rejected`,revoked:`Mark revoked`},Vr={user:`Account`,channel:`Channel`};function Hr({navigate:e}){let{can:t}=zt(),n=t(It),r=t(Pt),[i,a]=(0,g.useState)(`requests`),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)([]),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(!1);async function m(){d(``),p(!1);try{let[e,t]=await Promise.all([k.botVerifiers(new URLSearchParams({limit:`200`})),k.verificationIcons(new URLSearchParams({limit:`200`}))]);s(e.rows??[]),l(t.rows??[])}catch(e){if(e instanceof v&&e.status===403){s([]),l([]),p(!0);return}d(O(e))}}return(0,g.useEffect)(()=>{m()},[]),(0,W.jsxs)(Dt,{title:`Third-party verification`,eyebrow:`Third-party verification / Verifiers, icons, marks`,actions:r?(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>e(`/verification`),children:[(0,W.jsx)(Se,{size:15}),` `,`Official verification`]}):void 0,children:[u&&(0,W.jsx)(K,{children:u}),f&&(0,W.jsx)(K,{children:`The server refused the verifier roster and the icon catalogue for this session (403), so both lists are empty here — applications can still be reviewed.`}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`A verifier company's icon — not the official checkmark`,text:`A third-party mark is a verifier bot's own icon, drawn right BEFORE the name of an account, a bot or a channel, plus one line of description in the profile. It says “this verifier vouches for this peer”, and nothing more.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The icon is a custom emoji document. The client fetches it through messages.getCustomEmojiDocuments, so a document id that resolves to nothing renders as no badge at all — which is why marks are granted from the catalogue below rather than from a typed number.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The official checkmark is a different mechanism, granted by the platform in the Verification section. The two are stored, shown and taken away separately, and neither one implies the other.`}),!n&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This session can read the section and decide applications, but not change verifiers or the icon catalogue — that needs the botverification.manage permission.`})]}),(0,W.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Third-party verification`,children:[{key:`requests`,label:`Applications`,icon:(0,W.jsx)(tt,{size:15})},{key:`verifiers`,label:`Verifiers`,icon:(0,W.jsx)(pe,{size:15})},{key:`icons`,label:`Icon catalogue`,icon:(0,W.jsx)(nt,{size:15})},{key:`marks`,label:`Granted marks`,icon:(0,W.jsx)(ee,{size:15})}].map(e=>(0,W.jsxs)(`button`,{className:`btn icon-text ${i===e.key?`primary`:``}`,type:`button`,"aria-pressed":i===e.key,onClick:()=>a(e.key),children:[e.icon,` `,e.label]},e.key))}),i===`requests`&&(0,W.jsx)(Ur,{navigate:e,verifiers:o}),i===`verifiers`&&(0,W.jsx)(Wr,{verifiers:o,icons:c,canManage:n,onChanged:m,navigate:e}),i===`icons`&&(0,W.jsx)(Gr,{icons:c,verifiers:o,canManage:n,onChanged:m}),i===`marks`&&(0,W.jsx)(Kr,{verifiers:o,canManage:n,navigate:e})]})}function Ur({navigate:e,verifiers:t}){let[n,r]=(0,g.useState)(`pending`),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(`all`),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(`50`),[f,p]=(0,g.useState)([]),[m,h]=(0,g.useState)({}),[_,v]=(0,g.useState)(!1),[y,b]=(0,g.useState)(``),[x,S]=(0,g.useState)(!1),[C,w]=(0,g.useState)(``);async function T(e=!1){S(!0),w(``);let t=new URLSearchParams({limit:u});n!==`all`&&t.set(`status`,n),i&&t.set(`verifier_bot_id`,i),o!==`all`&&t.set(`peer_type`,o),c.trim()&&t.set(`q`,c.trim().replace(/^@/,``)),e&&y&&t.set(`before_id`,y);try{let n=await k.customVerificationRequests(t),r=n.rows??[];p(t=>e?[...t,...r]:r),b(n.next_before_id??``),v(!!n.has_more)}catch(e){w(O(e))}finally{S(!1)}}async function E(){try{h((await k.botVerificationCounts()).counts??{})}catch(e){w(O(e))}}(0,g.useEffect)(()=>{T(!1),E()},[]);function D(){T(!1),E()}return(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Application queue`,text:`Applications filed with a verifier bot by the owner of the peer. The counters cover the whole queue, not the page below.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:D,disabled:x,children:[(0,W.jsx)(qe,{size:15,className:x?`spin`:``}),` `,`Refresh`]})}),C&&(0,W.jsx)(K,{children:C}),(0,W.jsx)(`div`,{className:`metric-row`,children:Rr.map(e=>(0,W.jsx)(J,{label:Br[e],value:m[e]??`0`,mono:!0,tone:Xr(e,m[e]??`0`)},e))})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),T(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:`Application id, peer id, username or title`})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Status`}),(0,W.jsxs)(`select`,{value:n,onChange:e=>r(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All statuses`}),Rr.map(e=>(0,W.jsx)(`option`,{value:e,children:Br[e]},e))]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Verifier`}),(0,W.jsx)(qr,{value:i,verifiers:t,onChange:a})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Peer type`}),(0,W.jsxs)(`select`,{value:o,onChange:e=>s(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All types`}),zr.map(e=>(0,W.jsx)(`option`,{value:e,children:Vr[e]},e))]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:x,children:[x?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(B,{size:15}),` `,`Search`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Verifier`}),(0,W.jsx)(`th`,{children:`Peer`}),(0,W.jsx)(`th`,{children:`Applicant`}),(0,W.jsx)(`th`,{children:`Stated reason`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Filed`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[f.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:(0,W.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/bot-verification/${t.ID}`),children:[`#`,t.ID]})}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{children:H(t.VerifierBotUsername)||t.VerifierBotID}),(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:t.VerifierBotID})]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{children:Zr(t)}),(0,W.jsxs)(`div`,{className:`entity-subtitle mono`,children:[Vr[t.PeerType],` · `,t.PeerID]})]}),(0,W.jsxs)(`td`,{children:[H(t.ApplicantUsername)||`-`,(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:t.ApplicantUserID})]}),(0,W.jsx)(`td`,{className:`truncate`,children:t.Reason||`-`}),(0,W.jsx)(`td`,{children:(0,W.jsx)(Jr,{status:t.Status})}),(0,W.jsx)(`td`,{children:U(t.CreatedAt)||`-`}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/bot-verification/${t.ID}`),children:[(0,W.jsx)(tt,{size:14}),` `,`Details`,` `,(0,W.jsx)(ve,{size:14})]})})]},t.ID)),f.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})}),_&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>T(!0),disabled:x,children:[x?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(ge,{size:15}),` `,`Load more`]})})]})}function Wr({verifiers:e,icons:t,canManage:n,onChanged:r,navigate:i}){let[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(``),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(``),[h,_]=(0,g.useState)(!1),v=t.filter(e=>e.Active),y=v.map(e=>({value:e.DocumentID,label:`${e.Name} · ${e.DocumentID}`}));if(l&&!y.some(e=>e.value===l)){let e=t.find(e=>e.DocumentID===l);y.unshift({value:l,label:`${e?.Name??l} · ${l} (Retired)`})}function b(e){c(e),o(null),u(e.IconDocumentID),f(e.CompanyName),m(e.DefaultDescription),_(e.CanModifyCustomDescription)}function x(){c(null),o(null),u(``),f(``),m(``),_(!1)}function S(){return{bot_id:s?s.BotID:a?String(a.ID):`0`,icon_document_id:l||`0`,company_name:d.trim(),default_description:p.trim(),can_modify_custom_description:h,version:s?s.Version:`0`}}return(0,W.jsxs)(W.Fragment,{children:[n&&(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:s?`Update verifier`:`Grant verifier status`,text:`The bot gets an icon from the catalogue and a company name to vouch under. The same call updates an existing verifier, which is why it carries a version.`,action:s?(0,W.jsx)(`button`,{className:`btn icon-text`,type:`button`,onClick:x,children:`Cancel update`}):void 0}),s?(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Updating ${H(s.BotUsername)||s.BotID} — version ${s.Version} is sent as the optimistic lock, so a row somebody else changed meanwhile is refused instead of overwritten.`}):(0,W.jsx)(Nn,{label:`Bot`,value:a,onChange:o}),(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Icon from the catalogue`}),(0,W.jsxs)(`select`,{value:l,onChange:e=>u(e.target.value),children:[(0,W.jsx)(`option`,{value:``,children:`Pick an icon`}),y.map(e=>(0,W.jsx)(`option`,{value:e.value,children:e.label},e.value))]})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Company`}),(0,W.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),placeholder:`Acme Verification Ltd`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Default description`}),(0,W.jsx)(`input`,{value:p,onChange:e=>m(e.target.value),placeholder:`Verified by Acme`})]})]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:h,onChange:e=>_(e.target.checked)}),`The verifier may replace the description per peer`]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This is botVerifierSettings.can_modify_custom_description: with it off, every mark this verifier grants carries the default description above, whatever the applicant asked for.`}),v.length===0&&(0,W.jsx)(K,{children:`The catalogue has no active icon, so there is nothing to grant. Add one in the icon catalogue first.`}),(0,W.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,W.jsx)(`span`,{className:`bot-create-note`,children:`The bot can mark peers as soon as the row exists and is enabled.`}),(0,W.jsx)(Z,{label:s?`Update verifier`:`Grant verifier status`,icon:(0,W.jsx)(We,{size:15}),tone:`neutral`,path:`/api/actions/grant-bot-verifier`,payload:S,onDone:()=>{x(),r()}})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Verifier bots`,text:`Bots allowed to hand out their own mark. Verifier status is granted per deployment, so every row here is a badge printer an operator switched on by hand.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:r,children:[(0,W.jsx)(qe,{size:15}),` `,`Refresh`]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Bot`}),(0,W.jsx)(`th`,{children:`Company`}),(0,W.jsx)(`th`,{children:`Icon`}),(0,W.jsx)(`th`,{children:`Own description`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Marks`}),(0,W.jsx)(`th`,{children:`Granted by`}),(0,W.jsx)(`th`,{children:`Updated`}),n&&(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[e.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`button`,{className:`row-link`,type:`button`,onClick:()=>i(`/bots/${e.BotID}`),children:(0,W.jsx)(`strong`,{children:H(e.BotUsername)||e.BotName||e.BotID})}),(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:e.BotID})]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{children:e.CompanyName||`-`}),(0,W.jsx)(`div`,{className:`entity-subtitle truncate`,children:e.DefaultDescription||`Not set`})]}),(0,W.jsxs)(`td`,{children:[e.IconName||`-`,(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:e.IconDocumentID})]}),(0,W.jsx)(`td`,{children:e.CanModifyCustomDescription?`Yes`:`No`}),(0,W.jsx)(`td`,{children:e.Enabled?(0,W.jsx)(q,{tone:`good`,children:`Enabled`}):(0,W.jsx)(q,{tone:`warn`,children:`disabled`})}),(0,W.jsx)(`td`,{className:`mono`,children:String(e.MarkCount??`0`)}),(0,W.jsxs)(`td`,{children:[e.GrantedBy||`-`,(0,W.jsx)(`div`,{className:`entity-subtitle truncate`,children:e.GrantReason||`-`})]}),(0,W.jsx)(`td`,{children:U(e.UpdatedAt)||`-`}),n&&(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`row-actions`,children:[(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>b(e),children:`Edit`}),(0,W.jsx)(Z,{label:e.Enabled?`Disable`:`Enable`,icon:e.Enabled?(0,W.jsx)(Ge,{size:14}):(0,W.jsx)(z,{size:14}),tone:e.Enabled?`warn`:`neutral`,compact:!0,path:`/api/actions/set-bot-verifier-enabled`,payload:()=>({bot_id:e.BotID,enabled:!e.Enabled}),onDone:r}),(0,W.jsx)(Z,{label:`Revoke status`,icon:(0,W.jsx)(it,{size:14}),tone:`danger`,compact:!0,path:`/api/actions/revoke-bot-verifier`,payload:()=>({bot_id:e.BotID}),onDone:r})]})})]},e.BotID)),e.length===0&&(0,W.jsx)(jt,{colSpan:n?9:8})]})]})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Disabling is the per-verifier kill switch: the marks already granted keep rendering, but the bot can no longer mark anything new and its settings stop being projected into botInfo.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Revoking verifier status removes the row and every mark this verifier granted — the icon disappears from all of its peers at once.`})]})]})}function Gr({icons:e,verifiers:t,canManage:n,onChanged:r}){let[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``);function u(){let e={document_id:i.trim()||`0`,name:o.trim()};return c&&(e.owner_bot_id=c),e}return(0,W.jsxs)(W.Fragment,{children:[n&&(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{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.`}),(0,W.jsx)(Lr,{label:`Document`,value:i,onChange:a}),(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Name`}),(0,W.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:`Acme blue tick`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Owner`}),(0,W.jsxs)(`select`,{value:c,onChange:e=>l(e.target.value),children:[(0,W.jsx)(`option`,{value:``,children:`Shared`}),t.map(e=>(0,W.jsx)(`option`,{value:e.BotID,children:`${e.CompanyName||e.BotID} · ${H(e.BotUsername)||e.BotID}`},e.BotID))]})]})]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`A document id that resolves to nothing produces an invisible badge: the peer is marked in the database and the client draws nothing.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`A shared icon may be granted to any verifier; picking an owner reserves it for that one bot.`}),(0,W.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,W.jsx)(`span`,{className:`bot-create-note`,children:`Adding an icon grants nothing by itself — it only makes the document available to grant.`}),(0,W.jsx)(Z,{label:`Save icon`,icon:(0,W.jsx)(We,{size:15}),tone:`neutral`,path:`/api/actions/upsert-verification-icon`,payload:u,onDone:()=>{a(``),s(``),l(``),r()}})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Icon catalogue`,text:`The custom emoji documents a verifier may mark with. Nothing else can be used as an icon, so the catalogue is where a wrong badge is prevented rather than fixed.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:r,children:[(0,W.jsx)(qe,{size:15}),` `,`Refresh`]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Document ID`}),(0,W.jsx)(`th`,{children:`Name`}),(0,W.jsx)(`th`,{children:`Owner`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Verifiers using it`}),(0,W.jsx)(`th`,{children:`Filed`}),n&&(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[e.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:e.DocumentID}),(0,W.jsx)(`td`,{children:(0,W.jsx)(`strong`,{children:e.Name||`-`})}),(0,W.jsx)(`td`,{children:e.OwnerBotID&&e.OwnerBotID!==`0`?(0,W.jsxs)(W.Fragment,{children:[H(e.OwnerBotUsername)||e.OwnerBotID,(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:e.OwnerBotID})]}):(0,W.jsx)(q,{children:`Shared`})}),(0,W.jsx)(`td`,{children:e.Active?(0,W.jsx)(q,{tone:`good`,children:`Active`}):(0,W.jsx)(q,{tone:`warn`,children:`Retired`})}),(0,W.jsx)(`td`,{className:`mono`,children:String(e.UsedByVerifiers??`0`)}),(0,W.jsx)(`td`,{children:U(e.CreatedAt)||`-`}),n&&(0,W.jsx)(`td`,{children:(0,W.jsx)(`div`,{className:`row-actions`,children:(0,W.jsx)(Z,{label:e.Active?`Retire`:`Activate`,icon:e.Active?(0,W.jsx)(Ge,{size:14}):(0,W.jsx)(z,{size:14}),tone:e.Active?`warn`:`neutral`,compact:!0,path:`/api/actions/set-verification-icon-active`,payload:()=>({icon_id:e.ID,active:!e.Active}),onDone:r})})})]},e.ID)),e.length===0&&(0,W.jsx)(jt,{colSpan:n?7:6})]})]})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Retiring an icon stops it from being granted to anybody new. Marks already carrying it keep it: the icon is copied onto the mark when it is granted.`})]})]})}function Kr({verifiers:e,canManage:t,navigate:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(`all`),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(`50`),[d,f]=(0,g.useState)([]),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(!1),[b,x]=(0,g.useState)(``);async function S(e=!1){y(!0),x(``);let t=new URLSearchParams({limit:l});r&&t.set(`verifier_bot_id`,r),a!==`all`&&t.set(`peer_type`,a),s.trim()&&t.set(`q`,s.trim().replace(/^@/,``)),e&&h&&t.set(`before_id`,h);try{let n=await k.customVerifications(t),r=n.rows??[];f(t=>e?[...t,...r]:r),_(n.next_before_id??``),m(!!n.has_more)}catch(e){x(O(e))}finally{y(!1)}}return(0,g.useEffect)(()=>{S(!1)},[]),(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Granted marks`,text:`Every peer currently carrying a third-party mark, whoever granted it — an operator decision, the verifier bot itself, or the peer's owner through bots.setCustomVerification.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:v,children:[(0,W.jsx)(qe,{size:15,className:v?`spin`:``}),` `,`Refresh`]})}),b&&(0,W.jsx)(K,{children:b})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),S(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`Peer id, username or title`})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Verifier`}),(0,W.jsx)(qr,{value:r,verifiers:e,onChange:i})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Peer type`}),(0,W.jsxs)(`select`,{value:a,onChange:e=>o(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All types`}),zr.map(e=>(0,W.jsx)(`option`,{value:e,children:Vr[e]},e))]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsx)(`input`,{className:`small-input`,value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:v,children:[v?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(B,{size:15}),` `,`Search`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Verifier`}),(0,W.jsx)(`th`,{children:`Peer`}),(0,W.jsx)(`th`,{children:`Description`}),(0,W.jsx)(`th`,{children:`Icon`}),(0,W.jsx)(`th`,{children:`Filed`}),t&&(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[d.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsxs)(`td`,{className:`mono`,children:[`#`,e.ID]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{children:e.CompanyName||H(e.VerifierBotUsername)||e.VerifierBotID}),(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:H(e.VerifierBotUsername)||e.VerifierBotID})]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`button`,{className:`row-link`,type:`button`,onClick:()=>n(Qr(e.PeerType,e.PeerID)),children:(0,W.jsx)(`strong`,{children:Zr(e)})}),(0,W.jsxs)(`div`,{className:`entity-subtitle mono`,children:[Vr[e.PeerType],` · `,e.PeerID]})]}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Description||`Not set`}),(0,W.jsx)(`td`,{className:`mono`,children:e.IconDocumentID}),(0,W.jsx)(`td`,{children:U(e.CreatedAt)||`-`}),t&&(0,W.jsx)(`td`,{children:(0,W.jsx)(`div`,{className:`row-actions`,children:(0,W.jsx)(Z,{label:`Remove mark`,icon:(0,W.jsx)(fe,{size:14}),tone:`danger`,compact:!0,path:`/api/actions/revoke-custom-verification`,payload:()=>({verifier_bot_id:e.VerifierBotID,peer_type:e.PeerType,peer_id:e.PeerID}),onDone:()=>S(!1)})})})]},e.ID)),d.length===0&&(0,W.jsx)(jt,{colSpan:t?7:6})]})]})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Removing a mark clears the icon and the description from the peer. The application it came from keeps its history.`}),p&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!0),disabled:v,children:[v?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(ge,{size:15}),` `,`Load more`]})})]})}function qr({value:e,verifiers:t,onChange:n}){return(0,W.jsxs)(`select`,{value:e,onChange:e=>n(e.target.value),children:[(0,W.jsx)(`option`,{value:``,children:`All verifiers`}),t.map(e=>(0,W.jsx)(`option`,{value:e.BotID,children:`${e.CompanyName||e.BotID} · ${H(e.BotUsername)||e.BotID}`+(e.Enabled?``:` (disabled)`)},e.BotID))]})}function Jr({status:e}){return(0,W.jsx)(q,{tone:Yr(e),children:Br[e]})}function Yr(e){return e===`approved`?`good`:e===`pending`?`warn`:e===`rejected`?`danger`:`neutral`}function Xr(e,t){return e===`pending`?t!==`0`&&t!==``?`warn`:`neutral`:e===`approved`?`good`:`neutral`}function Zr(e){return H(e.PeerUsername)||e.PeerTitle||`#${e.PeerID}`}function Qr(e,t){return e===`channel`?`/channels/${t}`:`/accounts/${t}`}function $r({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);try{r(await k.customVerificationRequest(e))}catch(e){d(O(e))}finally{l(!1)}}function p(){s(!1),f()}(0,g.useEffect)(()=>{f()},[e]);function m(e){if(e instanceof v&&e.status===409)return s(!0),f(),`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}if(u&&!n)return(0,W.jsx)(K,{children:u});if(!n)return(0,W.jsx)(X,{label:`Loading the application…`});let h=n.request,_=ti(n.verifier),y=n.mark_active,b=h.Status===`pending`,x=h.Status===`approved`,S=i.trim(),C=h.RequestedDescription.trim(),w=!!_?.CanModifyCustomDescription&&C!==``,T=w?C:(_?.DefaultDescription??``).trim();function E(){let e={version:h.Version};return S&&(e.internal_note=S),e}function D(){a(``),s(!1),f()}return(0,W.jsxs)(Dt,{title:`Application #${h.ID}`,eyebrow:`Third-party verification / Review`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/bot-verification`),children:[(0,W.jsx)(ue,{size:15}),` `,`Back to list`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:p,disabled:c,children:[(0,W.jsx)(qe,{size:15,className:c?`spin`:``}),` `,`Refresh`]})]}),children:[u&&(0,W.jsx)(K,{children:u}),o&&(0,W.jsx)(K,{children:`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}),(0,W.jsx)(kt,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:Zr(h)}),(0,W.jsxs)(`div`,{className:`entity-subtitle mono`,children:[`#`,h.ID,` · `,Vr[h.PeerType],`:`,h.PeerID,` · v`,h.Version]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(Jr,{status:h.Status}),y?(0,W.jsxs)(q,{tone:`good`,children:[(0,W.jsx)(ee,{size:12}),` `,`Mark is live`]}):(0,W.jsx)(q,{tone:`neutral`,children:`No mark on the peer`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`A verifier company's icon — not the official checkmark`,text:`A third-party mark is a verifier bot's own icon, drawn right BEFORE the name of an account, a bot or a channel, plus one line of description in the profile. It says “this verifier vouches for this peer”, and nothing more.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The icon is a custom emoji document. The client fetches it through messages.getCustomEmojiDocuments, so a document id that resolves to nothing renders as no badge at all — which is why marks are granted from the catalogue below rather than from a typed number.`})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Verifier`,text:`The company whose icon the peer would carry, as its row stands right now.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/bots/${h.VerifierBotID}`),children:[(0,W.jsx)(pe,{size:15}),` `,`Open verifier bot`]})}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Company`,value:_?.CompanyName||`-`}),(0,W.jsx)(Y,{label:`Bot`,value:H(h.VerifierBotUsername)||`-`}),(0,W.jsx)(Y,{label:`Verifier bot ID`,value:h.VerifierBotID,mono:!0}),(0,W.jsx)(Y,{label:`Document ID`,value:_?.IconDocumentID||`-`,mono:!0}),(0,W.jsx)(Y,{label:`Name`,value:_?.IconName||`-`}),(0,W.jsx)(Y,{label:`Own description`,value:_?.CanModifyCustomDescription?`Yes`:`No`})]}),(0,W.jsx)(ei,{label:`Default description`,children:_?.DefaultDescription?(0,W.jsx)(`p`,{className:`about-text`,children:_.DefaultDescription}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),!_&&(0,W.jsx)(K,{children:`The verifier row is gone: its status was revoked after this application was filed. There is no icon to grant, so the application can only be rejected.`}),_&&!_.Enabled&&(0,W.jsx)(K,{children:`This verifier is disabled. It cannot mark anything new until an operator enables it again.`})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Peer`,text:`The account, bot or channel the icon would be attached to.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(Qr(h.PeerType,h.PeerID)),children:[(0,W.jsx)(Se,{size:15}),` `,`Open peer`]})}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Type`,value:Vr[h.PeerType]}),(0,W.jsx)(Y,{label:`Username`,value:H(h.PeerUsername)||`-`}),(0,W.jsx)(Y,{label:`Title`,value:h.PeerTitle||`-`}),(0,W.jsx)(Y,{label:`Peer ID`,value:h.PeerID,mono:!0})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Applicant`,text:`Who filed the application with the verifier bot.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/accounts/${h.ApplicantUserID}`),children:[(0,W.jsx)(st,{size:15}),` `,`Open account`]})}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Username`,value:H(h.ApplicantUsername)||`-`}),(0,W.jsx)(Y,{label:`User ID`,value:h.ApplicantUserID,mono:!0}),(0,W.jsx)(Y,{label:`Filed`,value:U(h.CreatedAt)||`-`}),(0,W.jsx)(Y,{label:`Updated`,value:U(h.UpdatedAt)||`-`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Application`,text:`What the applicant wrote, rendered as plain text.`}),(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Correlation ID`,value:h.CorrelationID||`-`,mono:!0}),(0,W.jsx)(Y,{label:`Status`,value:Br[h.Status]})]}),(0,W.jsx)(ei,{label:`Stated reason`,children:h.Reason?(0,W.jsx)(`p`,{className:`about-text`,children:h.Reason}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),(0,W.jsx)(ei,{label:`Requested description`,children:C?(0,W.jsx)(`p`,{className:`about-text`,children:C}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),(0,W.jsx)(ei,{label:`Description the mark would carry`,children:T?(0,W.jsx)(`p`,{className:`about-text`,children:T}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Resolved the same way the backend resolves it: the applicant's wording only when this verifier may set its own description, otherwise the verifier's default.`}),C!==``&&!w&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This verifier may not set a per-peer description, so the requested wording is ignored and the default is applied.`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Decision`,text:`What was decided, by whom, and with which wording.`}),(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Decided by`,value:h.DecidedBy||`-`}),(0,W.jsx)(Y,{label:`Approved`,value:U(h.ApprovedAt)||`-`}),(0,W.jsx)(Y,{label:`Rejected`,value:U(h.RejectedAt)||`-`}),(0,W.jsx)(Y,{label:`Version (optimistic lock)`,value:h.Version,mono:!0})]}),(0,W.jsx)(ei,{label:`Decision reason`,children:h.DecisionReason?(0,W.jsx)(`p`,{className:`about-text`,children:h.DecisionReason}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`No decision yet`})}),(0,W.jsx)(ei,{label:`Internal note · admins only`,children:h.InternalNote?(0,W.jsx)(`p`,{className:`about-text`,children:h.InternalNote}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})})]})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsxs)(`div`,{className:`dock-title`,children:[(0,W.jsx)(tt,{size:14}),` `,`Decision`]}),!b&&!x&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This status has no available actions.`}),(b||x)&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Internal note`}),(0,W.jsx)(`textarea`,{value:i,onChange:e=>a(e.target.value),rows:3,placeholder:`Handover note for other admins`})]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Optional. Stored with the decision and visible to admins only — never sent to the applicant.`})]}),b&&(0,W.jsxs)(W.Fragment,{children:[!_&&(0,W.jsx)(K,{children:`The verifier row is gone: its status was revoked after this application was filed. There is no icon to grant, so the application can only be rejected.`}),_&&!_.Enabled&&(0,W.jsx)(K,{children:`This verifier is disabled. It cannot mark anything new until an operator enables it again.`}),y&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This peer already carries this verifier's mark; approving refreshes the description and records the decision.`}),(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:`Approve`,icon:(0,W.jsx)(F,{size:15}),tone:`neutral`,path:`/api/botverification/requests/${h.ID}/approve`,payload:E,onDone:D,onError:m}),(0,W.jsx)(Z,{label:`Reject`,icon:(0,W.jsx)(ne,{size:15}),tone:`warn`,path:`/api/botverification/requests/${h.ID}/reject`,payload:E,onDone:D,onError:m})]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Puts the verifier's icon before the peer's name and its description in the profile, and messages the applicant.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The reason is mandatory: it is the wording the applicant is told, so write what exactly was missing.`})]}),x&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`div`,{className:`dock-title`,children:[(0,W.jsx)($e,{size:14}),` `,`Danger zone`]}),(0,W.jsxs)(`div`,{className:`danger-zone`,children:[(0,W.jsx)(Z,{label:`Revoke mark`,icon:(0,W.jsx)(fe,{size:15}),tone:`danger`,path:`/api/botverification/requests/${h.ID}/revoke`,payload:E,onDone:D,onError:m}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Takes the icon and the description off the peer and closes the application as revoked. The official checkmark, if the peer has one, is untouched.`}),!y&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The peer carries no mark right now — revoking only closes the application.`})]})]})]})})]})}function ei({label:e,children:t}){return(0,W.jsxs)(`div`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:e}),t]})}function ti(e){return!e||!e.BotID||e.BotID===`0`?null:e}var ni=[`draft`,`submitted`,`in_review`,`approved`,`rejected`,`cancelled`],ri=[`bot`,`channel`,`supergroup`,`user`],ii={draft:`Draft`,submitted:`Submitted`,in_review:`In review`,approved:`Approved`,rejected:`Rejected`,cancelled:`Cancelled`},ai={bot:`Bot`,channel:`Channel`,supergroup:`Supergroup`,user:`User`};function oi({navigate:e}){let[t,n]=(0,g.useState)(`all`),[r,i]=(0,g.useState)(`all`),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(`50`),[d,f]=(0,g.useState)([]),[p,m]=(0,g.useState)({}),[h,_]=(0,g.useState)(!1),[v,y]=(0,g.useState)(``),[b,x]=(0,g.useState)(!1),[S,C]=(0,g.useState)(``);async function w(e=!1){x(!0),C(``);let n=new URLSearchParams({limit:l});t!==`all`&&n.set(`status`,t),r!==`all`&&n.set(`target_type`,r),a.trim()&&n.set(`reviewer`,a.trim()),s.trim()&&n.set(`q`,s.trim().replace(/^@/,``)),e&&v&&n.set(`before_id`,v);try{let t=await k.verificationApplications(n),r=t.rows??[];f(t=>e?[...t,...r]:r),y(t.next_before_id??``),_(!!t.has_more)}catch(e){C(O(e))}finally{x(!1)}}async function T(){try{m((await k.verificationCounts()).counts??{})}catch(e){C(O(e))}}(0,g.useEffect)(()=>{w(!1),T()},[]);function E(){w(!1),T()}return(0,W.jsxs)(Dt,{title:`Verification queue`,eyebrow:`Verification / Queue`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:E,disabled:b,children:[(0,W.jsx)(qe,{size:15,className:b?`spin`:``}),` `,`Refresh`]}),children:[S&&(0,W.jsx)(K,{children:S}),(0,W.jsx)(`div`,{className:`metric-row`,children:ni.map(e=>(0,W.jsx)(J,{label:ii[e],value:p[e]??`0`,mono:!0,tone:li(e,p[e]??`0`)},e))}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),w(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(B,{size:15}),(0,W.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`Application id, peer id, username or title`})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Status`}),(0,W.jsxs)(`select`,{value:t,onChange:e=>n(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All statuses`}),ni.map(e=>(0,W.jsx)(`option`,{value:e,children:ii[e]},e))]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Target type`}),(0,W.jsxs)(`select`,{value:r,onChange:e=>i(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All types`}),ri.map(e=>(0,W.jsx)(`option`,{value:e,children:ai[e]},e))]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Reviewer`}),(0,W.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`Any reviewer`})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsx)(`input`,{className:`small-input`,value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:b,children:[b?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(B,{size:15}),` `,`Search`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Target`}),(0,W.jsx)(`th`,{children:`Applicant`}),(0,W.jsx)(`th`,{children:`Category`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Submitted`}),(0,W.jsx)(`th`,{children:`Reviewer`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[d.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:(0,W.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/verification/${t.ID}`),children:[`#`,t.ID]})}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{children:ui(t)}),(0,W.jsxs)(`div`,{className:`entity-subtitle mono`,children:[ai[t.TargetType],` · `,t.TargetID]}),t.TargetVerified&&(0,W.jsxs)(q,{tone:`good`,children:[(0,W.jsx)(ee,{size:12}),` `,`Badge already on`]})]}),(0,W.jsxs)(`td`,{children:[H(t.ApplicantUsername)||t.ApplicantName||`-`,(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:t.ApplicantUserID})]}),(0,W.jsx)(`td`,{children:t.Category||`-`}),(0,W.jsx)(`td`,{children:(0,W.jsx)(si,{status:t.Status})}),(0,W.jsx)(`td`,{children:U(t.SubmittedAt)||`-`}),(0,W.jsx)(`td`,{children:t.ReviewerAdminID||`-`}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/verification/${t.ID}`),children:[(0,W.jsx)(Qe,{size:14}),` `,`Details`,` `,(0,W.jsx)(ve,{size:14})]})})]},t.ID)),d.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})}),h&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>w(!0),disabled:b,children:[b?(0,W.jsx)(I,{size:15,className:`spin`}):(0,W.jsx)(ge,{size:15}),` `,`Load more`]})})]})}function si({status:e}){return(0,W.jsx)(q,{tone:ci(e),children:ii[e]})}function ci(e){return e===`approved`?`good`:e===`submitted`||e===`in_review`?`warn`:e===`rejected`?`danger`:`neutral`}function li(e,t){return e===`submitted`||e===`in_review`?t!==`0`&&t!==``?`warn`:`neutral`:e===`approved`?`good`:`neutral`}function ui(e){return H(e.TargetUsername)||e.TargetTitle||`#${e.TargetID}`}function di(e){return e.TargetType===`bot`?`/bots/${e.TargetID}`:e.TargetType===`user`?`/accounts/${e.TargetID}`:`/channels/${e.TargetID}`}var fi={created:`Created`,updated:`Updated`,submitted:`Submitted`,claimed:`Claimed`,approved:`Approved`,rejected:`Rejected`,cancelled:`Cancelled`,revoked:`Badge revoked`,notified:`Applicant notified`};function pi({id:e,navigate:t}){let{can:n}=zt(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(!1),[d,f]=(0,g.useState)(``);async function p(){u(!0),f(``);try{i(await k.verificationApplication(e))}catch(e){f(O(e))}finally{u(!1)}}function m(){c(!1),p()}(0,g.useEffect)(()=>{p()},[e]);function h(e){if(e instanceof v&&e.status===409)return c(!0),p(),`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}if(d&&!r)return(0,W.jsx)(K,{children:d});if(!r)return(0,W.jsx)(X,{label:`Loading the application…`});let _=r.application,y=r.events??[],b=r.applicant_controls_target,x=r.target_verified,S=_.Status===`submitted`,C=_.Status===`submitted`||_.Status===`in_review`,w=_.Status===`approved`&&n(`verification.revoke`),T=a.trim();function E(){let e={version:_.Version};return T&&(e.internal_note=T),e}function D(){o(``),c(!1),p()}return(0,W.jsxs)(Dt,{title:`Application #${_.ID}`,eyebrow:`Verification / Review`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/verification`),children:[(0,W.jsx)(ue,{size:15}),` `,`Back to list`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:m,disabled:l,children:[(0,W.jsx)(qe,{size:15,className:l?`spin`:``}),` `,`Refresh`]})]}),children:[d&&(0,W.jsx)(K,{children:d}),s&&(0,W.jsx)(K,{children:`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}),(0,W.jsx)(kt,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:ui(_)}),(0,W.jsxs)(`div`,{className:`entity-subtitle mono`,children:[`#`,_.ID,` · `,ai[_.TargetType],`:`,_.TargetID,` · v`,_.Version]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(si,{status:_.Status}),x&&(0,W.jsxs)(q,{tone:`good`,children:[(0,W.jsx)(ee,{size:12}),` `,`Badge already on`]}),(0,W.jsx)(q,{tone:b?`good`:`danger`,children:b?`Control confirmed`:`No control over the target`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Target`,text:`The peer the badge would be attached to, as it exists right now.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(di(_)),children:[(0,W.jsx)(Se,{size:15}),` `,`Open target`]})}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Type`,value:ai[_.TargetType]}),(0,W.jsx)(Y,{label:`Username`,value:H(_.TargetUsername)||`-`}),(0,W.jsx)(Y,{label:`Title`,value:_.TargetTitle||`-`}),(0,W.jsx)(Y,{label:`Peer ID`,value:_.TargetID,mono:!0})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Applicant`,text:`Who filed the application and whether they still hold rights on the target.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/accounts/${_.ApplicantUserID}`),children:[(0,W.jsx)(st,{size:15}),` `,`Open account`]})}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Username`,value:H(_.ApplicantUsername)||`-`}),(0,W.jsx)(Y,{label:`Name`,value:_.ApplicantName||`-`}),(0,W.jsx)(Y,{label:`User ID`,value:_.ApplicantUserID,mono:!0}),(0,W.jsx)(Y,{label:`Submitted`,value:U(_.SubmittedAt)||`-`})]}),b?(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The applicant controls the target right now — checked against the live records, not against the submission snapshot.`}):(0,W.jsx)(K,{children:`The applicant no longer controls the target. Approving would hand the badge to someone who does not hold the peer — normally a reason to reject.`})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Application`,text:`Everything the applicant submitted, rendered as plain text.`}),(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Category`,value:_.Category||`-`}),(0,W.jsx)(Y,{label:`Correlation ID`,value:_.CorrelationID||`-`,mono:!0}),(0,W.jsx)(Y,{label:`Created`,value:U(_.CreatedAt)||`-`}),(0,W.jsx)(Y,{label:`Updated`,value:U(_.UpdatedAt)||`-`})]}),(0,W.jsx)(mi,{label:`Description`,children:_.Description?(0,W.jsx)(`p`,{className:`about-text`,children:_.Description}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})}),(0,W.jsx)(mi,{label:`Official website`,children:_.OfficialWebsite?(0,W.jsx)(`div`,{className:`about-text`,children:(0,W.jsx)(hi,{value:_.OfficialWebsite})}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})}),(0,W.jsx)(mi,{label:`Social links`,children:(0,W.jsx)(gi,{values:_.SocialLinks})}),(0,W.jsx)(mi,{label:`Press coverage`,children:(0,W.jsx)(gi,{values:_.PressLinks})}),(0,W.jsx)(mi,{label:`Applicant comment`,children:_.AdditionalNote?(0,W.jsx)(`p`,{className:`about-text`,children:_.AdditionalNote}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Only http:// and https:// links are clickable and open in a new tab; anything else is shown as text.`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Decision`,text:`What was decided, by whom, and with which wording.`}),(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Reviewer`,value:_.ReviewerAdminID||`-`}),(0,W.jsx)(Y,{label:`Decided`,value:U(_.ReviewedAt)||`-`}),(0,W.jsx)(Y,{label:`Status`,value:ii[_.Status]}),(0,W.jsx)(Y,{label:`Version (optimistic lock)`,value:_.Version,mono:!0})]}),(0,W.jsx)(mi,{label:`Decision reason`,children:_.DecisionReason?(0,W.jsx)(`p`,{className:`about-text`,children:_.DecisionReason}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`No decision yet`})}),(0,W.jsx)(mi,{label:`Internal note · admins only`,children:_.InternalNote?(0,W.jsx)(`p`,{className:`about-text`,children:_.InternalNote}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`History`,text:`Immutable trail of every status transition, with actor and reason.`}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Event`}),(0,W.jsx)(`th`,{children:`From → to`}),(0,W.jsx)(`th`,{children:`Actor`}),(0,W.jsx)(`th`,{children:`Reason`}),(0,W.jsx)(`th`,{children:`Internal note`}),(0,W.jsx)(`th`,{children:`Time`})]})}),(0,W.jsxs)(`tbody`,{children:[y.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:(0,W.jsx)(_i,{kind:e.Kind})}),(0,W.jsxs)(`td`,{className:`mono`,children:[e.FromStatus||`-`,` → `,e.ToStatus||`-`]}),(0,W.jsx)(`td`,{children:e.Actor||`-`}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Reason||`-`}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Note||`-`}),(0,W.jsx)(`td`,{children:U(e.CreatedAt)||`-`})]},e.ID)),y.length===0&&(0,W.jsx)(jt,{colSpan:6})]})]})})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:`Review actions`}),!S&&!C&&!w&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This status has no available actions.`}),S&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsx)(`div`,{className:`action-stack`,children:(0,W.jsx)(Z,{label:`Take into review`,icon:(0,W.jsx)(De,{size:15}),tone:`neutral`,path:`/api/verification/applications/${_.ID}/claim`,payload:()=>({version:_.Version}),onDone:D,onError:h})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Assigns the application to you and moves it to in review, so two reviewers never work on the same one.`})]}),(C||w)&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Internal note`}),(0,W.jsx)(`textarea`,{value:a,onChange:e=>o(e.target.value),rows:3,placeholder:`Handover note for other reviewers`})]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Optional. Stored with the decision and visible to admins only — never sent to the applicant.`})]}),C&&(0,W.jsxs)(W.Fragment,{children:[!b&&(0,W.jsx)(K,{children:`The applicant no longer controls the target. Approving would hand the badge to someone who does not hold the peer — normally a reason to reject.`}),x&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The target already carries the badge; approving only records the decision.`}),(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:`Approve`,icon:(0,W.jsx)(F,{size:15}),tone:`neutral`,path:`/api/verification/applications/${_.ID}/approve`,payload:E,onDone:D,onError:h}),(0,W.jsx)(Z,{label:`Reject`,icon:(0,W.jsx)(ne,{size:15}),tone:`warn`,path:`/api/verification/applications/${_.ID}/reject`,payload:E,onDone:D,onError:h})]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Grants the official badge to the target and closes the application.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The reason is mandatory: it is the wording the applicant is told, so write what exactly was missing.`})]}),w&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`div`,{className:`dock-title`,children:[(0,W.jsx)($e,{size:14}),` `,`Danger zone`]}),(0,W.jsxs)(`div`,{className:`danger-zone`,children:[(0,W.jsx)(Z,{label:`Revoke verification`,icon:(0,W.jsx)(fe,{size:15}),tone:`danger`,path:`/api/actions/revoke-verification`,payload:()=>{let e={target_type:_.TargetType,target_id:_.TargetID};return T&&(e.internal_note=T),e},onDone:D,onError:h}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Clears the badge from the target. The approved application stays in history.`}),!x&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The target carries no badge right now — there is nothing to revoke.`})]})]})]})})]})}function mi({label:e,children:t}){return(0,W.jsxs)(`div`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:e}),t]})}function hi({value:e}){let t=ht(e);return t?(0,W.jsxs)(`a`,{className:`row-link`,href:t,target:`_blank`,rel:`noopener noreferrer`,children:[e,` `,(0,W.jsx)(Se,{size:13})]}):(0,W.jsx)(`span`,{className:`mono`,children:e})}function gi({values:e}){let t=(e??[]).filter(e=>e.trim()!==``);return t.length===0?(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`}):(0,W.jsx)(`div`,{className:`about-text`,children:t.map((e,t)=>(0,W.jsx)(`div`,{children:(0,W.jsx)(hi,{value:e})},`${t}-${e}`))})}function _i({kind:e}){return(0,W.jsx)(q,{tone:e===`approved`?`good`:e===`rejected`||e===`revoked`||e===`cancelled`?`danger`:e===`submitted`||e===`claimed`?`warn`:`neutral`,children:fi[e]})}function vi({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1],i=e.path.match(/^\/bots\/(\d+)$/)?.[1],a=e.path.match(/^\/moderation\/(\d+)$/)?.[1],o=e.path.match(/^\/collectible-usernames\/(\d+)$/)?.[1],s=e.path.match(/^\/verification\/(\d+)$/)?.[1],c=e.path.match(/^\/bot-verification\/(\d+)$/)?.[1];return c?(0,W.jsx)(Wt,{children:(0,W.jsx)(Ht,{permission:Ft,children:(0,W.jsx)($r,{id:c,navigate:t})})}):e.path===`/bot-verification`?(0,W.jsx)(Wt,{children:(0,W.jsx)(Ht,{permission:Ft,children:(0,W.jsx)(Hr,{navigate:t})})}):s?(0,W.jsx)(Ht,{permission:Pt,children:(0,W.jsx)(pi,{id:s,navigate:t})}):e.path===`/verification`?(0,W.jsx)(Ht,{permission:Pt,children:(0,W.jsx)(oi,{navigate:t})}):o?(0,W.jsx)(Bn,{id:o,navigate:t}):e.path===`/collectible-usernames`?(0,W.jsx)(In,{navigate:t}):e.path===`/storage`?(0,W.jsx)(Nr,{navigate:t}):n?(0,W.jsx)(wn,{id:Number(n),navigate:t}):r?(0,W.jsx)(Wn,{id:Number(r),navigate:t}):i?(0,W.jsx)(Jn,{id:Number(i),navigate:t}):a?(0,W.jsx)(Or,{id:Number(a),navigate:t}):e.path===`/accounts/shared-devices`?(0,W.jsx)(An,{navigate:t}):e.path===`/accounts`?(0,W.jsx)(kn,{navigate:t}):e.path===`/channels`?(0,W.jsx)(Kn,{navigate:t}):e.path===`/bots`?(0,W.jsx)(Zn,{navigate:t}):e.path===`/moderation`?(0,W.jsx)(xr,{navigate:t}):e.path===`/broadcasts`?(0,W.jsx)(er,{}):e.path===`/emoji`?(0,W.jsx)(hr,{kind:`emoji`}):e.path===`/stickers`?(0,W.jsx)(hr,{kind:`stickers`}):e.path===`/gif-catalog`?(0,W.jsx)(vr,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,W.jsx)(sr,{ownerUserID:Number(e.search.get(`owner_user_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups/detail`?(0,W.jsx)(ar,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups`?(0,W.jsx)(or,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,W.jsx)(cr,{navigate:t}):(0,W.jsx)(tr,{navigate:t})}function yi(){let[e,t]=(0,g.useState)(void 0),[n,r]=(0,g.useState)(()=>Gt());(0,g.useEffect)(()=>{let e=()=>r(Gt());return window.addEventListener(`popstate`,e),()=>window.removeEventListener(`popstate`,e)},[]),(0,g.useEffect)(()=>{k.session().then(e=>t(e)).catch(()=>t(null))},[]);let i=e=>{window.history.pushState(null,``,e),r(Gt())};return e===void 0?(0,W.jsx)(tn,{}):e===null?(0,W.jsx)(an,{onLogin:t}):(0,W.jsx)(Rt,{permissions:e.permissions??[],hideThirdPartyVerification:e.hide_third_party_verification??!0,children:(0,W.jsx)(nn,{actor:e.actor,route:n,navigate:i,onLogout:()=>t(null),children:(0,W.jsx)(vi,{route:n,navigate:i})})})}_.createRoot(document.getElementById(`root`)).render((0,W.jsx)(g.StrictMode,{children:(0,W.jsx)(Xt,{children:(0,W.jsx)(yi,{})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/assets/index-fn4QJaPB.js b/cmd/telesrv-admin/web/dist/assets/index-fn4QJaPB.js new file mode 100644 index 00000000..e135947b --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-fn4QJaPB.js @@ -0,0 +1,9 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.provider`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.iterator;function p(e){return typeof e!=`object`||!e?null:(e=f&&e[f]||e[`@@iterator`],typeof e==`function`?e:null)}var m={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},h=Object.assign,g={};function _(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}_.prototype.isReactComponent={},_.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`setState(...): takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},_.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function v(){}v.prototype=_.prototype;function y(e,t,n){this.props=e,this.context=t,this.refs=g,this.updater=n||m}var b=y.prototype=new v;b.constructor=y,h(b,_.prototype),b.isPureReactComponent=!0;var x=Array.isArray,S=Object.prototype.hasOwnProperty,C={current:null},w={key:!0,ref:!0,__self:!0,__source:!0};function T(e,n,r){var i,a={},o=null,s=null;if(n!=null)for(i in n.ref!==void 0&&(s=n.ref),n.key!==void 0&&(o=``+n.key),n)S.call(n,i)&&!w.hasOwnProperty(i)&&(a[i]=n[i]);var c=arguments.length-2;if(c===1)a.children=r;else if(1{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=typeof setTimeout==`function`?setTimeout:null,_=typeof clearTimeout==`function`?clearTimeout:null,v=typeof setImmediate<`u`?setImmediate:null;typeof navigator<`u`&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function y(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function b(e){if(h=!1,y(e),!m)if(n(c)!==null)m=!0,M(x);else{var t=n(l);t!==null&&N(b,t.startTime-e)}}function x(t,i){m=!1,h&&(h=!1,_(w),w=-1),p=!0;var a=f;try{for(y(i),d=n(c);d!==null&&(!(d.expirationTime>i)||t&&!D());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=i);i=e.unstable_now(),typeof s==`function`?d.callback=s:d===n(c)&&r(c),y(i)}else r(c);d=n(c)}if(d!==null)var u=!0;else{var g=n(l);g!==null&&N(b,g.startTime-i),u=!1}return u}finally{d=null,f=a,p=!1}}var S=!1,C=null,w=-1,T=5,E=-1;function D(){return!(e.unstable_now()-Ee||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(_(w),w=-1):h=!0,N(b,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,M(x))),r},e.unstable_shouldYield=D,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u(),n=f();function r(e){for(var t=`https://reactjs.org/docs/error-decoder.html?invariant=`+e,n=1;n`u`||window.document===void 0||window.document.createElement===void 0),l=Object.prototype.hasOwnProperty,d=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,p={},m={};function h(e){return l.call(m,e)?!0:l.call(p,e)?!1:d.test(e)?m[e]=!0:(p[e]=!0,!1)}function g(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case`function`:case`symbol`:return!0;case`boolean`:return r?!1:n===null?(e=e.toLowerCase().slice(0,5),e!==`data-`&&e!==`aria-`):!n.acceptsBooleans;default:return!1}}function _(e,t,n,r){if(t==null||g(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return!1===t;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function v(e,t,n,r,i,a,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=i,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=o}var y={};`children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style`.split(` `).forEach(function(e){y[e]=new v(e,0,!1,e,null,!1,!1)}),[[`acceptCharset`,`accept-charset`],[`className`,`class`],[`htmlFor`,`for`],[`httpEquiv`,`http-equiv`]].forEach(function(e){var t=e[0];y[t]=new v(t,1,!1,e[1],null,!1,!1)}),[`contentEditable`,`draggable`,`spellCheck`,`value`].forEach(function(e){y[e]=new v(e,2,!1,e.toLowerCase(),null,!1,!1)}),[`autoReverse`,`externalResourcesRequired`,`focusable`,`preserveAlpha`].forEach(function(e){y[e]=new v(e,2,!1,e,null,!1,!1)}),`allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope`.split(` `).forEach(function(e){y[e]=new v(e,3,!1,e.toLowerCase(),null,!1,!1)}),[`checked`,`multiple`,`muted`,`selected`].forEach(function(e){y[e]=new v(e,3,!0,e,null,!1,!1)}),[`capture`,`download`].forEach(function(e){y[e]=new v(e,4,!1,e,null,!1,!1)}),[`cols`,`rows`,`size`,`span`].forEach(function(e){y[e]=new v(e,6,!1,e,null,!1,!1)}),[`rowSpan`,`start`].forEach(function(e){y[e]=new v(e,5,!1,e.toLowerCase(),null,!1,!1)});var b=/[\-:]([a-z])/g;function x(e){return e[1].toUpperCase()}`accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,null,!1,!1)}),`xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type`.split(` `).forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/1999/xlink`,!1,!1)}),[`xml:base`,`xml:lang`,`xml:space`].forEach(function(e){var t=e.replace(b,x);y[t]=new v(t,1,!1,e,`http://www.w3.org/XML/1998/namespace`,!1,!1)}),[`tabIndex`,`crossOrigin`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!1,!1)}),y.xlinkHref=new v(`xlinkHref`,1,!1,`xlink:href`,`http://www.w3.org/1999/xlink`,!0,!1),[`src`,`href`,`action`,`formAction`].forEach(function(e){y[e]=new v(e,1,!1,e.toLowerCase(),null,!0,!0)});function S(e,t,n,r){var i=y.hasOwnProperty(t)?y[t]:null;(i===null?r||!(2s||i[o]!==a[s]){var c=` +`+i[o].replace(` at new `,` at `);return e.displayName&&c.includes(``)&&(c=c.replace(``,e.displayName)),c}while(1<=o&&0<=s);break}}}finally{ie=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:``)?re(e):``}function oe(e){switch(e.tag){case 5:return re(e.type);case 16:return re(`Lazy`);case 13:return re(`Suspense`);case 19:return re(`SuspenseList`);case 0:case 2:case 15:return e=ae(e.type,!1),e;case 11:return e=ae(e.type.render,!1),e;case 1:return e=ae(e.type,!0),e;default:return``}}function se(e){if(e==null)return null;if(typeof e==`function`)return e.displayName||e.name||null;if(typeof e==`string`)return e;switch(e){case E:return`Fragment`;case T:return`Portal`;case O:return`Profiler`;case D:return`StrictMode`;case M:return`Suspense`;case N:return`SuspenseList`}if(typeof e==`object`)switch(e.$$typeof){case A:return(e.displayName||`Context`)+`.Consumer`;case k:return(e._context.displayName||`Context`)+`.Provider`;case j:var t=e.render;return e=e.displayName,e||=(e=t.displayName||t.name||``,e===``?`ForwardRef`:`ForwardRef(`+e+`)`),e;case P:return t=e.displayName||null,t===null?se(e.type)||`Memo`:t;case F:t=e._payload,e=e._init;try{return se(e(t))}catch{}}return null}function ce(e){var t=e.type;switch(e.tag){case 24:return`Cache`;case 9:return(t.displayName||`Context`)+`.Consumer`;case 10:return(t._context.displayName||`Context`)+`.Provider`;case 18:return`DehydratedFragment`;case 11:return e=t.render,e=e.displayName||e.name||``,t.displayName||(e===``?`ForwardRef`:`ForwardRef(`+e+`)`);case 7:return`Fragment`;case 5:return t;case 4:return`Portal`;case 3:return`Root`;case 6:return`Text`;case 16:return se(t);case 8:return t===D?`StrictMode`:`Mode`;case 22:return`Offscreen`;case 12:return`Profiler`;case 21:return`Scope`;case 13:return`Suspense`;case 19:return`SuspenseList`;case 25:return`TracingMarker`;case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t==`function`)return t.displayName||t.name||null;if(typeof t==`string`)return t}return null}function le(e){switch(typeof e){case`boolean`:case`number`:case`string`:case`undefined`:return e;case`object`:return e;default:return``}}function ue(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()===`input`&&(t===`checkbox`||t===`radio`)}function de(e){var t=ue(e)?`checked`:`value`,n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=``+e[t];if(!e.hasOwnProperty(t)&&n!==void 0&&typeof n.get==`function`&&typeof n.set==`function`){var i=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(e){r=``+e,a.call(this,e)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(e){r=``+e},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function R(e){e._valueTracker||=de(e)}function fe(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r=``;return e&&(r=ue(e)?e.checked?`true`:`false`:e.value),e=r,e===n?!1:(t.setValue(e),!0)}function pe(e){if(e||=typeof document<`u`?document:void 0,e===void 0)return null;try{return e.activeElement||e.body}catch{return e.body}}function me(e,t){var n=t.checked;return L({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function he(e,t){var n=t.defaultValue==null?``:t.defaultValue,r=t.checked==null?t.defaultChecked:t.checked;n=le(t.value==null?n:t.value),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type===`checkbox`||t.type===`radio`?t.checked!=null:t.value!=null}}function ge(e,t){t=t.checked,t!=null&&S(e,`checked`,t,!1)}function _e(e,t){ge(e,t);var n=le(t.value),r=t.type;if(n!=null)r===`number`?(n===0&&e.value===``||e.value!=n)&&(e.value=``+n):e.value!==``+n&&(e.value=``+n);else if(r===`submit`||r===`reset`){e.removeAttribute(`value`);return}t.hasOwnProperty(`value`)?ye(e,t.type,n):t.hasOwnProperty(`defaultValue`)&&ye(e,t.type,le(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ve(e,t,n){if(t.hasOwnProperty(`value`)||t.hasOwnProperty(`defaultValue`)){var r=t.type;if(!(r!==`submit`&&r!==`reset`||t.value!==void 0&&t.value!==null))return;t=``+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==``&&(e.name=``),e.defaultChecked=!!e._wrapperState.initialChecked,n!==``&&(e.name=n)}function ye(e,t,n){(t!==`number`||pe(e.ownerDocument)!==e)&&(n==null?e.defaultValue=``+e._wrapperState.initialValue:e.defaultValue!==``+n&&(e.defaultValue=``+n))}var be=Array.isArray;function xe(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i`+t.valueOf().toString()+``,t=De.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function ke(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Ae={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},je=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(Ae).forEach(function(e){je.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Ae[t]=Ae[e]})});function Me(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||Ae.hasOwnProperty(e)&&Ae[e]?(``+t).trim():t+`px`}function Ne(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=Me(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var Pe=L({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Fe(e,t){if(t){if(Pe[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(r(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(r(60));if(typeof t.dangerouslySetInnerHTML!=`object`||!(`__html`in t.dangerouslySetInnerHTML))throw Error(r(61))}if(t.style!=null&&typeof t.style!=`object`)throw Error(r(62))}}function Ie(e,t){if(e.indexOf(`-`)===-1)return typeof t.is==`string`;switch(e){case`annotation-xml`:case`color-profile`:case`font-face`:case`font-face-src`:case`font-face-uri`:case`font-face-format`:case`font-face-name`:case`missing-glyph`:return!1;default:return!0}}var Le=null;function Re(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ze=null,Be=null,Ve=null;function He(e){if(e=Ai(e)){if(typeof ze!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Mi(t),ze(e.stateNode,e.type,t))}}function Ue(e){Be?Ve?Ve.push(e):Ve=[e]:Be=e}function We(){if(Be){var e=Be,t=Ve;if(Ve=Be=null,He(e),t)for(e=0;e>>=0,e===0?32:31-(Ct(e)/wt|0)|0}var Et=64,W=4194304;function Dt(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ot(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,i=e.suspendedLanes,a=e.pingedLanes,o=n&268435455;if(o!==0){var s=o&~i;s===0?(a&=o,a!==0&&(r=Dt(a))):r=Dt(s)}else o=n&~i,o===0?a!==0&&(r=Dt(a)):r=Dt(o);if(r===0)return 0;if(t!==0&&t!==r&&(t&i)===0&&(i=r&-r,a=t&-t,i>=a||i===16&&a&4194240))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Y(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-St(t),e[t]=n}function At(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Wn),qn=` `,Jn=!1;function Yn(e,t){switch(e){case`keyup`:return Hn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Xn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Zn=!1;function Qn(e,t){switch(e){case`compositionend`:return Xn(t);case`keypress`:return t.which===32?(Jn=!0,qn):null;case`textInput`:return e=t.data,e===qn&&Jn?null:e;default:return null}}function $n(e,t){if(Zn)return e===`compositionend`||!Un&&Yn(e,t)?(e=pn(),fn=dn=un=null,Zn=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=br(n)}}function Sr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Sr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Cr(){for(var e=window,t=pe();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=pe(e.document)}return t}function wr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}function Tr(e){var t=Cr(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Sr(n.ownerDocument.documentElement,n)){if(r!==null&&wr(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),`selectionStart`in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=n.textContent.length,a=Math.min(r.start,i);r=r.end===void 0?a:Math.min(r.end,i),!e.extend&&a>r&&(i=r,r=a,a=i),i=xr(n,a);var o=xr(n,r);i&&o&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==o.node||e.focusOffset!==o.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(o.node,o.offset)):(t.setEnd(o.node,o.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus==`function`&&n.focus(),n=0;n=document.documentMode,Dr=null,Or=null,kr=null,Ar=!1;function jr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ar||Dr==null||Dr!==pe(r)||(r=Dr,`selectionStart`in r&&wr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),kr&&yr(kr,r)||(kr=r,r=ri(Or,`onSelect`),0Pi||(e.current=Ni[Pi],Ni[Pi]=null,Pi--)}function Li(e,t){Pi++,Ni[Pi]=e.current,e.current=t}var Ri={},zi=Fi(Ri),Bi=Fi(!1),Vi=Ri;function Hi(e,t){var n=e.type.contextTypes;if(!n)return Ri;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},a;for(a in n)i[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Ui(e){return e=e.childContextTypes,e!=null}function Wi(){Ii(Bi),Ii(zi)}function Gi(e,t,n){if(zi.current!==Ri)throw Error(r(168));Li(zi,t),Li(Bi,n)}function Ki(e,t,n){var i=e.stateNode;if(t=t.childContextTypes,typeof i.getChildContext!=`function`)return n;for(var a in i=i.getChildContext(),i)if(!(a in t))throw Error(r(108,ce(e)||`Unknown`,a));return L({},n,i)}function qi(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ri,Vi=zi.current,Li(zi,e),Li(Bi,Bi.current),!0}function Ji(e,t,n){var i=e.stateNode;if(!i)throw Error(r(169));n?(e=Ki(e,t,Vi),i.__reactInternalMemoizedMergedChildContext=e,Ii(Bi),Ii(zi),Li(zi,e)):Ii(Bi),Li(Bi,n)}var Yi=null,Xi=!1,Zi=!1;function Qi(e){Yi===null?Yi=[e]:Yi.push(e)}function $i(e){Xi=!0,Qi(e)}function ea(){if(!Zi&&Yi!==null){Zi=!0;var e=0,t=X;try{var n=Yi;for(X=1;e>=o,i-=o,ca=1<<32-St(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(r,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(r,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(r,d),ga&&ua(r,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),ga&&ua(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return ga&&ua(a,g),u}for(h=i(a,h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),ga&&ua(a,g),u}function _(e,r,i,o){if(typeof i==`object`&&i&&i.type===E&&i.key===null&&(i=i.props.children),typeof i==`object`&&i){switch(i.$$typeof){case w:a:{for(var c=i.key,l=r;l!==null;){if(l.key===c){if(c=i.type,c===E){if(l.tag===7){n(e,l.sibling),r=a(l,i.props.children),r.return=e,e=r;break a}}else if(l.elementType===c||typeof c==`object`&&c&&c.$$typeof===F&&Aa(c)===l.type){n(e,l.sibling),r=a(l,i.props),r.ref=Oa(e,l,i),r.return=e,e=r;break a}n(e,l);break}else t(e,l);l=l.sibling}i.type===E?(r=Zl(i.props.children,e.mode,o,i.key),r.return=e,e=r):(o=Xl(i.type,i.key,i.props,null,e.mode,o),o.ref=Oa(e,r,i),o.return=e,e=o)}return s(e);case T:a:{for(l=i.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===i.containerInfo&&r.stateNode.implementation===i.implementation){n(e,r.sibling),r=a(r,i.children||[]),r.return=e,e=r;break a}else{n(e,r);break}else t(e,r);r=r.sibling}r=eu(i,e.mode,o),r.return=e,e=r}return s(e);case F:return l=i._init,_(e,r,l(i._payload),o)}if(be(i))return h(e,r,i,o);if(te(i))return g(e,r,i,o);ka(e,i)}return typeof i==`string`&&i!==``||typeof i==`number`?(i=``+i,r!==null&&r.tag===6?(n(e,r.sibling),r=a(r,i),r.return=e,e=r):(n(e,r),r=$l(i,e.mode,o),r.return=e,e=r),s(e)):n(e,r)}return _}var Ma=ja(!0),Na=ja(!1),Pa=Fi(null),Fa=null,Ia=null,La=null;function Ra(){La=Ia=Fa=null}function za(e){var t=Pa.current;Ii(Pa),e._currentValue=t}function Ba(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)===t?r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t):(e.childLanes|=t,r!==null&&(r.childLanes|=t)),e===n)break;e=e.return}}function Va(e,t){Fa=e,La=Ia=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(js=!0),e.firstContext=null)}function Ha(e){var t=e._currentValue;if(La!==e)if(e={context:e,memoizedValue:t,next:null},Ia===null){if(Fa===null)throw Error(r(308));Ia=e,Fa.dependencies={lanes:0,firstContext:e}}else Ia=Ia.next=e;return t}var Ua=null;function Wa(e){Ua===null?Ua=[e]:Ua.push(e)}function Ga(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,Wa(t)):(n.next=i.next,i.next=n),t.interleaved=n,Ka(e,r)}function Ka(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var qa=!1;function Ja(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ya(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Xa(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Za(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Bc&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,Ka(e,n)}return i=r.interleaved,i===null?(t.next=t,Wa(r)):(t.next=i.next,i.next=t),r.interleaved=t,Ka(e,n)}function Qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194240)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}function $a(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function eo(e,t,n,r){var i=e.updateQueue;qa=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane,p=s.eventTime;if((r&f)===f){u!==null&&(u=u.next={eventTime:p,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});a:{var m=e,h=s;switch(f=t,p=n,h.tag){case 1:if(m=h.payload,typeof m==`function`){d=m.call(p,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=h.payload,f=typeof m==`function`?m.call(p,d,f):m,f==null)break a;d=L({},d,f);break a;case 2:qa=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,f=i.effects,f===null?i.effects=[s]:f.push(s))}else p={eventTime:p,lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;f=s,s=f.next,f.next=null,i.lastBaseUpdate=f,i.shared.pending=null}}while(1);if(u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,t=i.shared.interleaved,t!==null){i=t;do o|=i.lane,i=i.next;while(i!==t)}else a===null&&(i.shared.lanes=0);Jc|=o,e.lanes=o,e.memoizedState=d}}function to(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=_o.transition;_o.transition={};try{e(!1),t()}finally{X=n,_o.transition=r}}function is(){return jo().memoizedState}function as(e,t,n){var r=pl(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},ss(e))cs(t,n);else if(n=Ga(e,t,n,r),n!==null){var i=fl();ml(n,e,r,i),ls(n,t,r)}}function os(e,t,n){var r=pl(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(ss(e))cs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Q(s,o)){var c=t.interleaved;c===null?(i.next=i,Wa(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}n=Ga(e,t,i,r),n!==null&&(i=fl(),ml(n,e,r,i),ls(n,t,r))}}function ss(e){var t=e.alternate;return e===yo||t!==null&&t===yo}function cs(e,t){Co=So=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function ls(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}var us={readContext:Ha,useCallback:Eo,useContext:Eo,useEffect:Eo,useImperativeHandle:Eo,useInsertionEffect:Eo,useLayoutEffect:Eo,useMemo:Eo,useReducer:Eo,useRef:Eo,useState:Eo,useDebugValue:Eo,useDeferredValue:Eo,useTransition:Eo,useMutableSource:Eo,useSyncExternalStore:Eo,useId:Eo,unstable_isNewReconciler:!1},ds={readContext:Ha,useCallback:function(e,t){return Ao().memoizedState=[e,t===void 0?null:t],e},useContext:Ha,useEffect:qo,useImperativeHandle:function(e,t,n){return n=n==null?null:n.concat([e]),Go(4194308,4,Zo.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Go(4194308,4,e,t)},useInsertionEffect:function(e,t){return Go(4,2,e,t)},useMemo:function(e,t){var n=Ao();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ao();return t=n===void 0?t:n(t),r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=as.bind(null,yo,e),[r.memoizedState,e]},useRef:function(e){var t=Ao();return e={current:e},t.memoizedState=e},useState:Ho,useDebugValue:$o,useDeferredValue:function(e){return Ao().memoizedState=e},useTransition:function(){var e=Ho(!1),t=e[0];return e=rs.bind(null,e[1]),Ao().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var i=yo,a=Ao();if(ga){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Vc===null)throw Error(r(349));vo&30||Lo(i,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,qo(zo.bind(null,i,o,e),[e]),i.flags|=2048,Uo(9,Ro.bind(null,i,o,n,t),void 0,null),n},useId:function(){var e=Ao(),t=Vc.identifierPrefix;if(ga){var n=la,r=ca;n=(r&~(1<<32-St(r)-1)).toString(32)+n,t=`:`+t+`R`+n,n=wo++,0<\/script>`,e=e.removeChild(e.firstChild)):typeof i.is==`string`?e=c.createElement(n,{is:i.is}):(e=c.createElement(n),n===`select`&&(c=e,i.multiple?c.multiple=!0:i.size&&(c.size=i.size))):e=c.createElementNS(e,n),e[Ci]=t,e[wi]=i,tc(e,t,!1,!1),t.stateNode=e;a:{switch(c=Ie(n,i),n){case`dialog`:Xr(`cancel`,e),Xr(`close`,e),o=i;break;case`iframe`:case`object`:case`embed`:Xr(`load`,e),o=i;break;case`video`:case`audio`:for(o=0;oel&&(t.flags|=128,i=!0,ic(s,!1),t.lanes=4194304)}else{if(!i)if(e=po(c),e!==null){if(t.flags|=128,i=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),ic(s,!0),s.tail===null&&s.tailMode===`hidden`&&!c.alternate&&!ga)return ac(t),null}else 2*pt()-s.renderingStartTime>el&&n!==1073741824&&(t.flags|=128,i=!0,ic(s,!1),t.lanes=4194304);s.isBackwards?(c.sibling=t.child,t.child=c):(n=s.last,n===null?t.child=c:n.sibling=c,s.last=c)}return s.tail===null?(ac(t),null):(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=pt(),t.sibling=null,n=fo.current,Li(fo,i?n&1|2:n&1),t);case 22:case 23:return wl(),i=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==i&&(t.flags|=8192),i&&t.mode&1?Wc&1073741824&&(ac(t),t.subtreeFlags&6&&(t.flags|=8192)):ac(t),null;case 24:return null;case 25:return null}throw Error(r(156,t.tag))}function sc(e,t){switch(pa(t),t.tag){case 1:return Ui(t.type)&&Wi(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return co(),Ii(Bi),Ii(zi),ho(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return uo(t),null;case 13:if(Ii(fo),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));Ta()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Ii(fo),null;case 4:return co(),null;case 10:return za(t.type._context),null;case 22:case 23:return wl(),null;case 24:return null;default:return null}}var cc=!1,lc=!1,uc=typeof WeakSet==`function`?WeakSet:Set,$=null;function dc(e,t){var n=e.ref;if(n!==null)if(typeof n==`function`)try{n(null)}catch(n){Rl(e,t,n)}else n.current=null}function fc(e,t,n){try{n()}catch(n){Rl(e,t,n)}}var pc=!1;function mc(e,t){if(di=rn,e=Cr(),wr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var i=n.getSelection&&n.getSelection();if(i&&i.rangeCount!==0){n=i.anchorNode;var a=i.anchorOffset,o=i.focusNode;i=i.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||i!==0&&f.nodeType!==3||(l=s+i),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===i&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(fi={focusedElem:e,selectionRange:n},rn=!1,$=t;$!==null;)if(t=$,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,$=e;else for(;$!==null;){t=$;try{var h=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(h!==null){var g=h.memoizedProps,_=h.memoizedState,v=t.stateNode;v.__reactInternalSnapshotBeforeUpdate=v.getSnapshotBeforeUpdate(t.elementType===t.type?g:ms(t.type,g),_)}break;case 3:var y=t.stateNode.containerInfo;y.nodeType===1?y.textContent=``:y.nodeType===9&&y.documentElement&&y.removeChild(y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(r(163))}}catch(e){Rl(t,t.return,e)}if(e=t.sibling,e!==null){e.return=t.return,$=e;break}$=t.return}return h=pc,pc=!1,h}function hc(e,t,n){var r=t.updateQueue;if(r=r===null?null:r.lastEffect,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var a=i.destroy;i.destroy=void 0,a!==void 0&&fc(t,n,a)}i=i.next}while(i!==r)}}function gc(e,t){if(t=t.updateQueue,t=t===null?null:t.lastEffect,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function _c(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t==`function`?t(e):t.current=e}}function vc(e){var t=e.alternate;t!==null&&(e.alternate=null,vc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ci],delete t[wi],delete t[Ei],delete t[Di],delete t[Oi])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function yc(e){return e.tag===5||e.tag===3||e.tag===4}function bc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||yc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function xc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=ui));else if(r!==4&&(e=e.child,e!==null))for(xc(e,t,n),e=e.sibling;e!==null;)xc(e,t,n),e=e.sibling}function Sc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Sc(e,t,n),e=e.sibling;e!==null;)Sc(e,t,n),e=e.sibling}var Cc=null,wc=!1;function Tc(e,t,n){for(n=n.child;n!==null;)Ec(e,t,n),n=n.sibling}function Ec(e,t,n){if(bt&&typeof bt.onCommitFiberUnmount==`function`)try{bt.onCommitFiberUnmount(yt,n)}catch{}switch(n.tag){case 5:lc||dc(n,t);case 6:var r=Cc,i=wc;Cc=null,Tc(e,t,n),Cc=r,wc=i,Cc!==null&&(wc?(e=Cc,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Cc.removeChild(n.stateNode));break;case 18:Cc!==null&&(wc?(e=Cc,n=n.stateNode,e.nodeType===8?yi(e.parentNode,n):e.nodeType===1&&yi(e,n),tn(e)):yi(Cc,n.stateNode));break;case 4:r=Cc,i=wc,Cc=n.stateNode.containerInfo,wc=!0,Tc(e,t,n),Cc=r,wc=i;break;case 0:case 11:case 14:case 15:if(!lc&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var a=i,o=a.destroy;a=a.tag,o!==void 0&&(a&2||a&4)&&fc(n,t,o),i=i.next}while(i!==r)}Tc(e,t,n);break;case 1:if(!lc&&(dc(n,t),r=n.stateNode,typeof r.componentWillUnmount==`function`))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(e){Rl(n,t,e)}Tc(e,t,n);break;case 21:Tc(e,t,n);break;case 22:n.mode&1?(lc=(r=lc)||n.memoizedState!==null,Tc(e,t,n),lc=r):Tc(e,t,n);break;default:Tc(e,t,n)}}function Dc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new uc),t.forEach(function(t){var r=Hl.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))})}}function Oc(e,t){var n=t.deletions;if(n!==null)for(var i=0;ia&&(a=s),i&=~o}if(i=a,i=pt()-i,i=(120>i?120:480>i?480:1080>i?1080:1920>i?1920:3e3>i?3e3:4320>i?4320:1960*Ic(i/1960))-i,10e?16:e,ol===null)var i=!1;else{if(e=ol,ol=null,sl=0,Bc&6)throw Error(r(331));var a=Bc;for(Bc|=4,$=e.current;$!==null;){var o=$,s=o.child;if($.flags&16){var c=o.deletions;if(c!==null){for(var l=0;lpt()-$c?Tl(e,0):Xc|=n),hl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=W,W<<=1,!(W&130023424)&&(W=4194304)):t=1);var n=fl();e=Ka(e,t),e!==null&&(Y(e,t,n),hl(e,n))}function Vl(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Bl(e,n)}function Hl(e,t){var n=0;switch(e.tag){case 13:var i=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:i=e.stateNode;break;default:throw Error(r(314))}i!==null&&i.delete(t),Bl(e,n)}var Ul=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Bi.current)js=!0;else{if((e.lanes&n)===0&&!(t.flags&128))return js=!1,ec(e,t,n);js=!!(e.flags&131072)}else js=!1,ga&&t.flags&1048576&&da(t,ia,t.index);switch(t.lanes=0,t.tag){case 2:var i=t.type;Qs(e,t),e=t.pendingProps;var a=Hi(t,zi.current);Va(t,n),a=Oo(null,t,i,e,a,n);var o=ko();return t.flags|=1,typeof a==`object`&&a&&typeof a.render==`function`&&a.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Ui(i)?(o=!0,qi(t)):o=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,Ja(t),a.updater=gs,t.stateNode=a,a._reactInternals=t,bs(t,i,e,n),t=Bs(null,t,i,!0,o,n)):(t.tag=0,ga&&o&&fa(t),Ms(null,t,a,n),t=t.child),t;case 16:i=t.elementType;a:{switch(Qs(e,t),e=t.pendingProps,a=i._init,i=a(i._payload),t.type=i,a=t.tag=Jl(i),e=ms(i,e),a){case 0:t=Rs(null,t,i,e,n);break a;case 1:t=zs(null,t,i,e,n);break a;case 11:t=Ns(null,t,i,e,n);break a;case 14:t=Ps(null,t,i,ms(i.type,e),n);break a}throw Error(r(306,i,``))}return t;case 0:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Rs(e,t,i,a,n);case 1:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),zs(e,t,i,a,n);case 3:a:{if(Vs(t),e===null)throw Error(r(387));i=t.pendingProps,o=t.memoizedState,a=o.element,Ya(e,t),eo(t,i,null,n);var s=t.memoizedState;if(i=s.element,o.isDehydrated)if(o={element:i,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){a=xs(Error(r(423)),t),t=Hs(e,t,i,n,a);break a}else if(i!==a){a=xs(Error(r(424)),t),t=Hs(e,t,i,n,a);break a}else for(ha=bi(t.stateNode.containerInfo.firstChild),ma=t,ga=!0,_a=null,n=Na(t,null,i,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Ta(),i===a){t=$s(e,t,n);break a}Ms(e,t,i,n)}t=t.child}return t;case 5:return lo(t),e===null&&xa(t),i=t.type,a=t.pendingProps,o=e===null?null:e.memoizedProps,s=a.children,pi(i,a)?s=null:o!==null&&pi(i,o)&&(t.flags|=32),Ls(e,t),Ms(e,t,s,n),t.child;case 6:return e===null&&xa(t),null;case 13:return Gs(e,t,n);case 4:return so(t,t.stateNode.containerInfo),i=t.pendingProps,e===null?t.child=Ma(t,null,i,n):Ms(e,t,i,n),t.child;case 11:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Ns(e,t,i,a,n);case 7:return Ms(e,t,t.pendingProps,n),t.child;case 8:return Ms(e,t,t.pendingProps.children,n),t.child;case 12:return Ms(e,t,t.pendingProps.children,n),t.child;case 10:a:{if(i=t.type._context,a=t.pendingProps,o=t.memoizedProps,s=a.value,Li(Pa,i._currentValue),i._currentValue=s,o!==null)if(Q(o.value,s)){if(o.children===a.children&&!Bi.current){t=$s(e,t,n);break a}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var c=o.dependencies;if(c!==null){s=o.child;for(var l=c.firstContext;l!==null;){if(l.context===i){if(o.tag===1){l=Xa(-1,n&-n),l.tag=2;var u=o.updateQueue;if(u!==null){u=u.shared;var d=u.pending;d===null?l.next=l:(l.next=d.next,d.next=l),u.pending=l}}o.lanes|=n,l=o.alternate,l!==null&&(l.lanes|=n),Ba(o.return,n,t),c.lanes|=n;break}l=l.next}}else if(o.tag===10)s=o.type===t.type?null:o.child;else if(o.tag===18){if(s=o.return,s===null)throw Error(r(341));s.lanes|=n,c=s.alternate,c!==null&&(c.lanes|=n),Ba(s,n,t),s=o.sibling}else s=o.child;if(s!==null)s.return=o;else for(s=o;s!==null;){if(s===t){s=null;break}if(o=s.sibling,o!==null){o.return=s.return,s=o;break}s=s.return}o=s}Ms(e,t,a.children,n),t=t.child}return t;case 9:return a=t.type,i=t.pendingProps.children,Va(t,n),a=Ha(a),i=i(a),t.flags|=1,Ms(e,t,i,n),t.child;case 14:return i=t.type,a=ms(i,t.pendingProps),a=ms(i.type,a),Ps(e,t,i,a,n);case 15:return Fs(e,t,t.type,t.pendingProps,n);case 17:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:ms(i,a),Qs(e,t),t.tag=1,Ui(i)?(e=!0,qi(t)):e=!1,Va(t,n),vs(t,i,a),bs(t,i,a,n),Bs(null,t,i,!0,e,n);case 19:return Zs(e,t,n);case 22:return Is(e,t,n)}throw Error(r(156,t.tag))};function Wl(e,t){return ut(e,t)}function Gl(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Kl(e,t,n,r){return new Gl(e,t,n,r)}function ql(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Jl(e){if(typeof e==`function`)return+!!ql(e);if(e!=null){if(e=e.$$typeof,e===j)return 11;if(e===P)return 14}return 2}function Yl(e,t){var n=e.alternate;return n===null?(n=Kl(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Xl(e,t,n,i,a,o){var s=2;if(i=e,typeof e==`function`)ql(e)&&(s=1);else if(typeof e==`string`)s=5;else a:switch(e){case E:return Zl(n.children,a,o,t);case D:s=8,a|=8;break;case O:return e=Kl(12,n,t,a|2),e.elementType=O,e.lanes=o,e;case M:return e=Kl(13,n,t,a),e.elementType=M,e.lanes=o,e;case N:return e=Kl(19,n,t,a),e.elementType=N,e.lanes=o,e;case ee:return Ql(n,a,o,t);default:if(typeof e==`object`&&e)switch(e.$$typeof){case k:s=10;break a;case A:s=9;break a;case j:s=11;break a;case P:s=14;break a;case F:s=16,i=null;break a}throw Error(r(130,e==null?e:typeof e,``))}return t=Kl(s,n,t,a),t.elementType=e,t.type=i,t.lanes=o,t}function Zl(e,t,n,r){return e=Kl(7,e,r,t),e.lanes=n,e}function Ql(e,t,n,r){return e=Kl(22,e,r,t),e.elementType=ee,e.lanes=n,e.stateNode={isHidden:!1},e}function $l(e,t,n){return e=Kl(6,e,null,t),e.lanes=n,e}function eu(e,t,n){return t=Kl(4,e.children===null?[]:e.children,e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function tu(e,t,n,r,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=J(0),this.expirationTimes=J(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=J(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function nu(e,t,n,r,i,a,o,s,c){return e=new tu(e,t,n,s,c),t===1?(t=1,!0===a&&(t|=8)):t=0,a=Kl(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ja(a),e}function ru(e,t,n){var r=3{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=m();e.createRoot=t.createRoot,e.hydrateRoot=t.hydrateRoot})),g=c(u()),_=c(h(),1),v=class extends Error{status;constructor(e,t){super(t),this.status=e}},y=`telesrv_admin_csrf`,b=`X-CSRF-Token`,x=``;function S(e){x=(e??``).trim()}function C(){if(typeof document>`u`)return``;for(let e of document.cookie.split(`;`)){let t=e.trim(),n=t.indexOf(`=`);if(!(n<=0||t.slice(0,n)!==y))try{return decodeURIComponent(t.slice(n+1))}catch{return t.slice(n+1)}}return``}function w(){return C()||x}function T(e){let t=(e??`GET`).toUpperCase();return t!==`GET`&&t!==`HEAD`&&t!==`OPTIONS`}function E(e){if(!e)return{};if(e instanceof Headers){let t={};return e.forEach((e,n)=>{t[n]=e}),t}return Array.isArray(e)?Object.fromEntries(e):{...e}}async function D(e,t={}){let n=typeof FormData<`u`&&t.body instanceof FormData?{}:{"Content-Type":`application/json`};if(Object.assign(n,E(t.headers)),T(t.method)){let e=w();e&&(n[b]=e)}let r=await fetch(e,{credentials:`same-origin`,...t,headers:n}),i=await r.text(),a=i?JSON.parse(i):null;if(!r.ok){let e=a?.error||a?.Error||a?.message||r.statusText;throw new v(r.status,e)}return a}function O(e){return e instanceof Error?e.message:String(e)}var k={session:()=>D(`/api/session`),login:async e=>{let t=await D(`/api/login`,{method:`POST`,body:JSON.stringify({secret:e})});return S(t.csrf_token),t},logout:()=>D(`/api/logout`,{method:`POST`,body:`{}`}),accounts:e=>D(`/api/accounts?${e.toString()}`),accountStats:()=>D(`/api/accounts/stats`),sharedDeviceGroups:e=>D(`/api/accounts/shared-devices?${e.toString()}`),account:e=>D(`/api/accounts/${e}`),channels:e=>D(`/api/channels?${e.toString()}`),channel:e=>D(`/api/channels/${e}`),bots:e=>D(`/api/bots?${e.toString()}`),broadcasts:e=>D(`/api/broadcasts?${e.toString()}`),bot:e=>D(`/api/bots/${e}`),collectibleUsernames:e=>D(`/api/collectible-usernames?${e.toString()}`),collectibleUsername:e=>D(`/api/collectible-usernames/${encodeURIComponent(e)}`),reservedUsernames:e=>D(`/api/reserved-usernames?${e.toString()}`),dashboard:()=>D(`/api/dashboard`),storageStats:()=>D(`/api/storage/stats`),storageAccounts:e=>D(`/api/storage/accounts?${e.toString()}`),verificationApplications:e=>D(`/api/verification/applications?${e.toString()}`),verificationApplication:e=>D(`/api/verification/applications/${encodeURIComponent(e)}`),verificationCounts:()=>D(`/api/verification/counts`),botVerifiers:e=>D(`/api/botverification/verifiers?${e.toString()}`),verificationIcons:e=>D(`/api/botverification/icons?${e.toString()}`),customVerifications:e=>D(`/api/botverification/marks?${e.toString()}`),customVerificationRequests:e=>D(`/api/botverification/requests?${e.toString()}`),customVerificationRequest:e=>D(`/api/botverification/requests/${encodeURIComponent(e)}`),botVerificationCounts:()=>D(`/api/botverification/counts`),emoji:e=>D(`/api/emoji?${e.toString()}`),emojiAnimation:e=>D(`/api/emoji/${encodeURIComponent(e)}/animation`),messages:e=>D(`/api/messages?${e.toString()}`),message:(e,t)=>D(`/api/messages/detail?${new URLSearchParams({owner_user_id:String(e),msg_id:String(t)}).toString()}`),groupMessages:e=>D(`/api/messages/groups?${e.toString()}`),groupMessage:(e,t)=>D(`/api/messages/groups/detail?${new URLSearchParams({channel_id:String(e),msg_id:String(t)}).toString()}`),moderationCases:e=>D(`/api/moderation/cases?${e.toString()}`),moderationCase:e=>D(`/api/moderation/cases/${e}`),moderationReport:e=>D(`/api/moderation/reports/${e}`),claimModerationCase:(e,t)=>D(`/api/moderation/cases/${e}/claim`,{method:`POST`,body:JSON.stringify({expected_version:t})}),decideModerationCase:(e,t)=>D(`/api/moderation/cases/${e}/decide`,{method:`POST`,body:JSON.stringify(t)}),reviewModerationAppeal:(e,t,n)=>D(`/api/moderation/cases/${e}/appeals/${t}/review`,{method:`POST`,body:JSON.stringify(n)}),stickerSets:e=>D(`/api/stickers?kind=${encodeURIComponent(e)}`),stickerSetDocuments:e=>D(`/api/stickers/${encodeURIComponent(e)}/documents`),stickerDocumentAnimationURL:e=>`/api/stickers/documents/${encodeURIComponent(e)}/animation`,gifCatalogDocumentPreviewURL:e=>`/api/gif-catalog/documents/${encodeURIComponent(e)}/preview`,createStickerSet:e=>D(`/api/actions/create-sticker-set`,{method:`POST`,body:e}),setAccountAvatar:e=>D(`/api/actions/set-account-avatar`,{method:`POST`,body:e}),setChannelAvatar:e=>D(`/api/actions/set-channel-avatar`,{method:`POST`,body:e}),addStickerToSet:e=>D(`/api/actions/add-sticker-to-set`,{method:`POST`,body:e}),gifCatalog:()=>D(`/api/gif-catalog`),createGifCatalogEntry:e=>D(`/api/actions/create-gif-catalog-entry`,{method:`POST`,body:e}),action:(e,t)=>D(e,{method:`POST`,body:JSON.stringify(t)})},A=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),j=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),M={xmlns:`http://www.w3.org/2000/svg`,width:24,height:24,viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:2,strokeLinecap:`round`,strokeLinejoin:`round`},N=(0,g.forwardRef)(({color:e=`currentColor`,size:t=24,strokeWidth:n=2,absoluteStrokeWidth:r,className:i=``,children:a,iconNode:o,...s},c)=>(0,g.createElement)(`svg`,{ref:c,...M,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:j(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),P=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(N,{ref:i,iconNode:t,className:j(`lucide-${A(e)}`,n),...r}));return n.displayName=`${e}`,n},F=P(`BadgeCheck`,[[`path`,{d:`M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z`,key:`3c2336`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),ee=P(`CircleAlert`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`line`,{x1:`12`,x2:`12`,y1:`8`,y2:`12`,key:`1pkeuh`}],[`line`,{x1:`12`,x2:`12.01`,y1:`16`,y2:`16`,key:`4dfq90`}]]),I=P(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),te=P(`CircleX`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m15 9-6 6`,key:`1uzhvr`}],[`path`,{d:`m9 9 6 6`,key:`z0biqf`}]]),L=P(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),ne=P(`ShieldX`,[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`m14.5 9.5-5 5`,key:`17q4r4`}],[`path`,{d:`m9.5 9.5 5 5`,key:`18nt4w`}]]),re=P(`Sparkles`,[[`path`,{d:`M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z`,key:`4pj2yx`}],[`path`,{d:`M20 3v4`,key:`1olli1`}],[`path`,{d:`M22 5h-4`,key:`1gvqau`}],[`path`,{d:`M4 17v2`,key:`vumght`}],[`path`,{d:`M5 18H3`,key:`zchphs`}]]),ie=P(`TriangleAlert`,[[`path`,{d:`m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3`,key:`wmoenq`}],[`path`,{d:`M12 9v4`,key:`juzpu7`}],[`path`,{d:`M12 17h.01`,key:`p32p05`}]]),ae=P(`UserRound`,[[`circle`,{cx:`12`,cy:`8`,r:`5`,key:`1hypcn`}],[`path`,{d:`M20 21a8 8 0 0 0-16 0`,key:`rfgkzh`}]]),oe=P(`UsersRound`,[[`path`,{d:`M18 21a8 8 0 0 0-16 0`,key:`3ypg7q`}],[`circle`,{cx:`10`,cy:`8`,r:`5`,key:`o932ke`}],[`path`,{d:`M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3`,key:`10s06x`}]]),se=P(`Activity`,[[`path`,{d:`M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2`,key:`169zse`}]]),ce=P(`ArrowLeftRight`,[[`path`,{d:`M8 3 4 7l4 4`,key:`9rb6wj`}],[`path`,{d:`M4 7h16`,key:`6tx8e3`}],[`path`,{d:`m16 21 4-4-4-4`,key:`siv7j2`}],[`path`,{d:`M20 17H4`,key:`h6l3hr`}]]),le=P(`ArrowLeft`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]),ue=P(`AtSign`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8`,key:`7n84p3`}]]),de=P(`Ban`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m4.9 4.9 14.2 14.2`,key:`1m5liu`}]]),R=P(`Bot`,[[`path`,{d:`M12 8V4H8`,key:`hb8ula`}],[`rect`,{width:`16`,height:`12`,x:`4`,y:`8`,rx:`2`,key:`enze0r`}],[`path`,{d:`M2 14h2`,key:`vft8re`}],[`path`,{d:`M20 14h2`,key:`4cs60a`}],[`path`,{d:`M15 13v2`,key:`1xurst`}],[`path`,{d:`M9 13v2`,key:`rq6x2g`}]]),fe=P(`Building2`,[[`path`,{d:`M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z`,key:`1b4qmf`}],[`path`,{d:`M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2`,key:`i71pzd`}],[`path`,{d:`M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2`,key:`10jefs`}],[`path`,{d:`M10 6h4`,key:`1itunk`}],[`path`,{d:`M10 10h4`,key:`tcdvrf`}],[`path`,{d:`M10 14h4`,key:`kelpxr`}],[`path`,{d:`M10 18h4`,key:`1ulq68`}]]),pe=P(`Cable`,[[`path`,{d:`M17 21v-2a1 1 0 0 1-1-1v-1a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v1a1 1 0 0 1-1 1`,key:`10bnsj`}],[`path`,{d:`M19 15V6.5a1 1 0 0 0-7 0v11a1 1 0 0 1-7 0V9`,key:`1eqmu1`}],[`path`,{d:`M21 21v-2h-4`,key:`14zm7j`}],[`path`,{d:`M3 5h4V3`,key:`z442eg`}],[`path`,{d:`M7 5a1 1 0 0 1 1 1v1a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a1 1 0 0 1 1-1V3`,key:`ebdjd7`}]]),me=P(`Check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),he=P(`ChevronDown`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),ge=P(`ChevronLeft`,[[`path`,{d:`m15 18-6-6 6-6`,key:`1wnfg3`}]]),_e=P(`ChevronRight`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),ve=P(`Copy`,[[`rect`,{width:`14`,height:`14`,x:`8`,y:`8`,rx:`2`,ry:`2`,key:`17jyea`}],[`path`,{d:`M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2`,key:`zix9uf`}]]),ye=P(`Cpu`,[[`rect`,{width:`16`,height:`16`,x:`4`,y:`4`,rx:`2`,key:`14l7u7`}],[`rect`,{width:`6`,height:`6`,x:`9`,y:`9`,rx:`1`,key:`5aljv4`}],[`path`,{d:`M15 2v2`,key:`13l42r`}],[`path`,{d:`M15 20v2`,key:`15mkzm`}],[`path`,{d:`M2 15h2`,key:`1gxd5l`}],[`path`,{d:`M2 9h2`,key:`1bbxkp`}],[`path`,{d:`M20 15h2`,key:`19e6y8`}],[`path`,{d:`M20 9h2`,key:`19tzq7`}],[`path`,{d:`M9 2v2`,key:`165o2o`}],[`path`,{d:`M9 20v2`,key:`i2bqo8`}]]),be=P(`Database`,[[`ellipse`,{cx:`12`,cy:`5`,rx:`9`,ry:`3`,key:`msslwz`}],[`path`,{d:`M3 5V19A9 3 0 0 0 21 19V5`,key:`1wlel7`}],[`path`,{d:`M3 12A9 3 0 0 0 21 12`,key:`mv7ke4`}]]),xe=P(`ExternalLink`,[[`path`,{d:`M15 3h6v6`,key:`1q9fwt`}],[`path`,{d:`M10 14 21 3`,key:`gplh6r`}],[`path`,{d:`M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6`,key:`a6xqqp`}]]),Se=P(`Eye`,[[`path`,{d:`M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0`,key:`1nclc0`}],[`circle`,{cx:`12`,cy:`12`,r:`3`,key:`1v7zrd`}]]),z=P(`FileJson`,[[`path`,{d:`M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z`,key:`1rqfz7`}],[`path`,{d:`M14 2v4a2 2 0 0 0 2 2h4`,key:`tnqrlb`}],[`path`,{d:`M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1`,key:`1oajmo`}],[`path`,{d:`M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1`,key:`mpwhp6`}]]),Ce=P(`Film`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`path`,{d:`M7 3v18`,key:`bbkbws`}],[`path`,{d:`M3 7.5h4`,key:`zfgn84`}],[`path`,{d:`M3 12h18`,key:`1i2n21`}],[`path`,{d:`M3 16.5h4`,key:`1230mu`}],[`path`,{d:`M17 3v18`,key:`in4fa5`}],[`path`,{d:`M17 7.5h4`,key:`myr1c1`}],[`path`,{d:`M17 16.5h4`,key:`go4c1d`}]]),we=P(`Flag`,[[`path`,{d:`M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z`,key:`i9b6wo`}],[`line`,{x1:`4`,x2:`4`,y1:`22`,y2:`15`,key:`1cm3nv`}]]),Te=P(`Flame`,[[`path`,{d:`M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z`,key:`96xj49`}]]),Ee=P(`Handshake`,[[`path`,{d:`m11 17 2 2a1 1 0 1 0 3-3`,key:`efffak`}],[`path`,{d:`m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4`,key:`9pr0kb`}],[`path`,{d:`m21 3 1 11h-2`,key:`1tisrp`}],[`path`,{d:`M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3`,key:`1uvwmv`}],[`path`,{d:`M3 4h8`,key:`1ep09j`}]]),De=P(`HardDrive`,[[`line`,{x1:`22`,x2:`2`,y1:`12`,y2:`12`,key:`1y58io`}],[`path`,{d:`M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z`,key:`oot6mr`}],[`line`,{x1:`6`,x2:`6.01`,y1:`16`,y2:`16`,key:`sgf278`}],[`line`,{x1:`10`,x2:`10.01`,y1:`16`,y2:`16`,key:`1l4acy`}]]),Oe=P(`History`,[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`,key:`1357e3`}],[`path`,{d:`M3 3v5h5`,key:`1xhq8a`}],[`path`,{d:`M12 7v5l4 2`,key:`1fdv2h`}]]),ke=P(`ImageOff`,[[`line`,{x1:`2`,x2:`22`,y1:`2`,y2:`22`,key:`a6p6uj`}],[`path`,{d:`M10.41 10.41a2 2 0 1 1-2.83-2.83`,key:`1bzlo9`}],[`line`,{x1:`13.5`,x2:`6`,y1:`13.5`,y2:`21`,key:`1q0aeu`}],[`line`,{x1:`18`,x2:`21`,y1:`12`,y2:`15`,key:`5mozeu`}],[`path`,{d:`M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59`,key:`mmje98`}],[`path`,{d:`M21 15V5a2 2 0 0 0-2-2H9`,key:`43el77`}]]),Ae=P(`ImagePlus`,[[`path`,{d:`M16 5h6`,key:`1vod17`}],[`path`,{d:`M19 2v6`,key:`4bpg5p`}],[`path`,{d:`M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5`,key:`1ue2ih`}],[`path`,{d:`m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21`,key:`1xmnt7`}],[`circle`,{cx:`9`,cy:`9`,r:`2`,key:`af1f0g`}]]),je=P(`LayoutDashboard`,[[`rect`,{width:`7`,height:`9`,x:`3`,y:`3`,rx:`1`,key:`10lvy0`}],[`rect`,{width:`7`,height:`5`,x:`14`,y:`3`,rx:`1`,key:`16une8`}],[`rect`,{width:`7`,height:`9`,x:`14`,y:`12`,rx:`1`,key:`1hutg5`}],[`rect`,{width:`7`,height:`5`,x:`3`,y:`16`,rx:`1`,key:`ldoo1y`}]]),Me=P(`LifeBuoy`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m4.93 4.93 4.24 4.24`,key:`1ymg45`}],[`path`,{d:`m14.83 9.17 4.24-4.24`,key:`1cb5xl`}],[`path`,{d:`m14.83 14.83 4.24 4.24`,key:`q42g0n`}],[`path`,{d:`m9.17 14.83-4.24 4.24`,key:`bqpfvv`}],[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}]]),Ne=P(`LogOut`,[[`path`,{d:`M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4`,key:`1uf3rs`}],[`polyline`,{points:`16 17 21 12 16 7`,key:`1gabdz`}],[`line`,{x1:`21`,x2:`9`,y1:`12`,y2:`12`,key:`1uyos4`}]]),Pe=P(`Mail`,[[`rect`,{width:`20`,height:`16`,x:`2`,y:`4`,rx:`2`,key:`18n3k1`}],[`path`,{d:`m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7`,key:`1ocrg3`}]]),Fe=P(`Megaphone`,[[`path`,{d:`m3 11 18-5v12L3 14v-3z`,key:`n962bs`}],[`path`,{d:`M11.6 16.8a3 3 0 1 1-5.8-1.6`,key:`1yl0tm`}]]),Ie=P(`MemoryStick`,[[`path`,{d:`M6 19v-3`,key:`1nvgqn`}],[`path`,{d:`M10 19v-3`,key:`iu8nkm`}],[`path`,{d:`M14 19v-3`,key:`kcehxu`}],[`path`,{d:`M18 19v-3`,key:`1vh91z`}],[`path`,{d:`M8 11V9`,key:`63erz4`}],[`path`,{d:`M16 11V9`,key:`fru6f3`}],[`path`,{d:`M12 11V9`,key:`ha00sb`}],[`path`,{d:`M2 15h20`,key:`16ne18`}],[`path`,{d:`M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.1a2 2 0 0 0 0 3.837V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.1a2 2 0 0 0 0-3.837Z`,key:`lhddv3`}]]),Le=P(`MessageSquareText`,[[`path`,{d:`M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z`,key:`1lielz`}],[`path`,{d:`M13 8H7`,key:`14i4kc`}],[`path`,{d:`M17 12H7`,key:`16if0g`}]]),Re=P(`MonitorSmartphone`,[[`path`,{d:`M18 8V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8`,key:`10dyio`}],[`path`,{d:`M10 19v-3.96 3.15`,key:`1irgej`}],[`path`,{d:`M7 19h5`,key:`qswx4l`}],[`rect`,{width:`6`,height:`10`,x:`16`,y:`12`,rx:`2`,key:`1egngj`}]]),ze=P(`Moon`,[[`path`,{d:`M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z`,key:`a7tn18`}]]),Be=P(`Palette`,[[`circle`,{cx:`13.5`,cy:`6.5`,r:`.5`,fill:`currentColor`,key:`1okk4w`}],[`circle`,{cx:`17.5`,cy:`10.5`,r:`.5`,fill:`currentColor`,key:`f64h9f`}],[`circle`,{cx:`8.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`fotxhn`}],[`circle`,{cx:`6.5`,cy:`12.5`,r:`.5`,fill:`currentColor`,key:`qy21gx`}],[`path`,{d:`M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z`,key:`12rzf8`}]]),Ve=P(`Phone`,[[`path`,{d:`M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z`,key:`foiqr5`}]]),He=P(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),Ue=P(`Plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),We=P(`PowerOff`,[[`path`,{d:`M18.36 6.64A9 9 0 0 1 20.77 15`,key:`dxknvb`}],[`path`,{d:`M6.16 6.16a9 9 0 1 0 12.68 12.68`,key:`1x7qb5`}],[`path`,{d:`M12 2v4`,key:`3427ic`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}]]),B=P(`Power`,[[`path`,{d:`M12 2v10`,key:`mnfbl`}],[`path`,{d:`M18.4 6.6a9 9 0 1 1-12.77.04`,key:`obofu9`}]]),Ge=P(`Radio`,[[`path`,{d:`M4.9 19.1C1 15.2 1 8.8 4.9 4.9`,key:`1vaf9d`}],[`path`,{d:`M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5`,key:`u1ii0m`}],[`circle`,{cx:`12`,cy:`12`,r:`2`,key:`1c9p78`}],[`path`,{d:`M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5`,key:`1j5fej`}],[`path`,{d:`M19.1 4.9C23 8.8 23 15.1 19.1 19`,key:`10b0cb`}]]),Ke=P(`RefreshCw`,[[`path`,{d:`M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8`,key:`v9h5vc`}],[`path`,{d:`M21 3v5h-5`,key:`1q7to0`}],[`path`,{d:`M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16`,key:`3uifl3`}],[`path`,{d:`M8 16H3v5`,key:`1cv678`}]]),qe=P(`ScrollText`,[[`path`,{d:`M15 12h-5`,key:`r7krc0`}],[`path`,{d:`M15 8h-5`,key:`1khuty`}],[`path`,{d:`M19 17V5a2 2 0 0 0-2-2H4`,key:`zz82l3`}],[`path`,{d:`M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3`,key:`1ph1d7`}]]),V=P(`Search`,[[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}],[`path`,{d:`m21 21-4.3-4.3`,key:`1qie3q`}]]),Je=P(`Send`,[[`path`,{d:`M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z`,key:`1ffxy3`}],[`path`,{d:`m21.854 2.147-10.94 10.939`,key:`12cjpa`}]]),Ye=P(`Settings2`,[[`path`,{d:`M20 7h-9`,key:`3s1dr2`}],[`path`,{d:`M14 17H5`,key:`gfn3mx`}],[`circle`,{cx:`17`,cy:`17`,r:`3`,key:`18b49y`}],[`circle`,{cx:`7`,cy:`7`,r:`3`,key:`dfmy0x`}]]),Xe=P(`ShieldAlert`,[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`M12 8v4`,key:`1got3b`}],[`path`,{d:`M12 16h.01`,key:`1drbdi`}]]),Ze=P(`ShieldCheck`,[[`path`,{d:`M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z`,key:`oel41y`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),Qe=P(`ShieldOff`,[[`path`,{d:`m2 2 20 20`,key:`1ooewy`}],[`path`,{d:`M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71`,key:`1jlk70`}],[`path`,{d:`M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264`,key:`18rp1v`}]]),$e=P(`Smartphone`,[[`rect`,{width:`14`,height:`20`,x:`5`,y:`2`,rx:`2`,ry:`2`,key:`1yt0o3`}],[`path`,{d:`M12 18h.01`,key:`mhygvu`}]]),et=P(`Smile`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`M8 14s1.5 2 4 2 4-2 4-2`,key:`1y1vjs`}],[`line`,{x1:`9`,x2:`9.01`,y1:`9`,y2:`9`,key:`yxxnd0`}],[`line`,{x1:`15`,x2:`15.01`,y1:`9`,y2:`9`,key:`1p4y9e`}]]),tt=P(`Stamp`,[[`path`,{d:`M5 22h14`,key:`ehvnwv`}],[`path`,{d:`M19.27 13.73A2.5 2.5 0 0 0 17.5 13h-11A2.5 2.5 0 0 0 4 15.5V17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-1.5c0-.66-.26-1.3-.73-1.77Z`,key:`1sy9ra`}],[`path`,{d:`M14 13V8.5C14 7 15 7 15 5a3 3 0 0 0-3-3c-1.66 0-3 1-3 3s1 2 1 3.5V13`,key:`cnxgux`}]]),nt=P(`Sticker`,[[`path`,{d:`M15.5 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V8.5L15.5 3Z`,key:`1wis1t`}],[`path`,{d:`M14 3v4a2 2 0 0 0 2 2h4`,key:`36rjfy`}],[`path`,{d:`M8 13h.01`,key:`1sbv64`}],[`path`,{d:`M16 13h.01`,key:`wip0gl`}],[`path`,{d:`M10 16s.8 1 2 1c1.3 0 2-1 2-1`,key:`1vvgv3`}]]),rt=P(`Sun`,[[`circle`,{cx:`12`,cy:`12`,r:`4`,key:`4exip2`}],[`path`,{d:`M12 2v2`,key:`tus03m`}],[`path`,{d:`M12 20v2`,key:`1lh1kg`}],[`path`,{d:`m4.93 4.93 1.41 1.41`,key:`149t6j`}],[`path`,{d:`m17.66 17.66 1.41 1.41`,key:`ptbguv`}],[`path`,{d:`M2 12h2`,key:`1t8f8n`}],[`path`,{d:`M20 12h2`,key:`1q8mjw`}],[`path`,{d:`m6.34 17.66-1.41 1.41`,key:`1m8zz5`}],[`path`,{d:`m19.07 4.93-1.41 1.41`,key:`1shlcs`}]]),it=P(`Trash2`,[[`path`,{d:`M3 6h18`,key:`d0wm0j`}],[`path`,{d:`M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6`,key:`4alrt4`}],[`path`,{d:`M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2`,key:`v07s0e`}],[`line`,{x1:`10`,x2:`10`,y1:`11`,y2:`17`,key:`1uufr5`}],[`line`,{x1:`14`,x2:`14`,y1:`11`,y2:`17`,key:`xtxkd`}]]),at=P(`Undo2`,[[`path`,{d:`M9 14 4 9l5-5`,key:`102s5s`}],[`path`,{d:`M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11`,key:`f3b9sd`}]]),ot=P(`Upload`,[[`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4`,key:`ih7n3h`}],[`polyline`,{points:`17 8 12 3 7 8`,key:`t8dd8p`}],[`line`,{x1:`12`,x2:`12`,y1:`3`,y2:`15`,key:`widbto`}]]),st=P(`User`,[[`path`,{d:`M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2`,key:`975kel`}],[`circle`,{cx:`12`,cy:`7`,r:`4`,key:`17ys0d`}]]),ct=P(`Users`,[[`path`,{d:`M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2`,key:`1yyitq`}],[`circle`,{cx:`9`,cy:`7`,r:`4`,key:`nufk8`}],[`path`,{d:`M22 21v-2a4 4 0 0 0-3-3.87`,key:`kshegd`}],[`path`,{d:`M16 3.13a4 4 0 0 1 0 7.75`,key:`1da9ce`}]]),lt=P(`Vault`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`circle`,{cx:`7.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`kqv944`}],[`path`,{d:`m7.9 7.9 2.7 2.7`,key:`hpeyl3`}],[`circle`,{cx:`16.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`w0ekpg`}],[`path`,{d:`m13.4 10.6 2.7-2.7`,key:`264c1n`}],[`circle`,{cx:`7.5`,cy:`16.5`,r:`.5`,fill:`currentColor`,key:`nkw3mc`}],[`path`,{d:`m7.9 16.1 2.7-2.7`,key:`p81g5e`}],[`circle`,{cx:`16.5`,cy:`16.5`,r:`.5`,fill:`currentColor`,key:`fubopw`}],[`path`,{d:`m13.4 13.4 2.7 2.7`,key:`abhel3`}],[`circle`,{cx:`12`,cy:`12`,r:`2`,key:`1c9p78`}]]),ut=P(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]);function dt(e){let t=e.trim();return!t||t.startsWith(`+`)?t:/^\d+$/.test(t)?`+${t}`:t}function H(e){let t=e.trim();return t?t.startsWith(`@`)?t:`@${t}`:``}function ft(e){return`${e.FirstName||``} ${e.LastName||``}`.trim()||`-`}function pt(e){return e.Broadcast&&!e.Megagroup?`Channel`:e.Megagroup&&e.Forum?`Supergroup / Forum`:e.Megagroup?`Supergroup`:`Channel / Group`}function U(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function mt(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function ht(e){let t=(e??``).trim();if(!/^https?:\/\//i.test(t))return``;try{let e=new URL(t);return e.protocol!==`http:`&&e.protocol!==`https:`?``:e.href}catch{return``}}function gt(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function _t(e){let t=(e??``).trim();if(!t)return`0`;let n=Number(t);return Number.isFinite(n)?n.toLocaleString():t}var vt={XTR:0,TON:9,USD:2,EUR:2,RUB:2};function yt(e){let t=(e??``).trim().toUpperCase();return t in vt?vt[t]:2}function bt(e,t){let n=(e??``).trim();if(!n)return`0`;if(!/^-?\d+$/.test(n))return n;let r=yt(t),i=n.startsWith(`-`),a=(i?n.slice(1):n).replace(/^0+(?=\d)/,``).padStart(r+1,`0`),o=a.slice(0,a.length-r)||`0`,s=r>0?a.slice(a.length-r):``;r>2&&(s=s.replace(/0+$/,``));let c=i?`-`:``;return s?`${c}${xt(o)}.${s}`:`${c}${xt(o)}`}function xt(e){return e.replace(/\B(?=(\d{3})+(?!\d))/g,` `)}function St(e,t){let n=(t??``).trim().toUpperCase(),r=bt(e,n);return n?`${r} ${n}`:r}function Ct(e,t){let n=(e??``).trim().replace(/\s+/g,``).replace(`,`,`.`);if(!n)return`0`;if(!/^\d*(\.\d*)?$/.test(n)||n===`.`)return null;let r=yt(t),[i,a=``]=n.split(`.`);if(a.length>r)return null;let o=`${i||`0`}${a.padEnd(r,`0`)}`.replace(/^0+(?=\d)/,``);return o===``?`0`:o}function wt(e){let t=(e??``).trim();if(!t||!/^\d+$/.test(t))return`0 B`;let n=Number(t);if(!Number.isFinite(n))return`${t} B`;let r=[`B`,`KB`,`MB`,`GB`,`TB`,`PB`],i=n,a=0;for(;i>=1024&&ae.trim()).filter(Boolean).map(e=>Number.parseInt(e,10));if(n.length===0||n.some(e=>!Number.isFinite(e)||e<=0))throw Error(t);return n}var Et=o((e=>{var t=u(),n=Symbol.for(`react.element`),r=Symbol.for(`react.fragment`),i=Object.prototype.hasOwnProperty,a=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,o={key:!0,ref:!0,__self:!0,__source:!0};function s(e,t,r){var s,c={},l=null,u=null;for(s in r!==void 0&&(l=``+r),t.key!==void 0&&(l=``+t.key),t.ref!==void 0&&(u=t.ref),t)i.call(t,s)&&!o.hasOwnProperty(s)&&(c[s]=t[s]);if(e&&e.defaultProps)for(s in t=e.defaultProps,t)c[s]===void 0&&(c[s]=t[s]);return{$$typeof:n,type:e,key:l,ref:u,props:c,_owner:a.current}}e.Fragment=r,e.jsx=s,e.jsxs=s})),W=o(((e,t)=>{t.exports=Et()}))();function Dt({title:e,eyebrow:t,children:n,actions:r}){return(0,W.jsxs)(`div`,{className:`page-frame`,children:[(0,W.jsxs)(`div`,{className:`page-title-row`,children:[(0,W.jsxs)(`div`,{children:[t&&(0,W.jsx)(`div`,{className:`eyebrow`,children:t}),(0,W.jsx)(`h2`,{children:e})]}),r&&(0,W.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Ot({children:e}){return(0,W.jsx)(`div`,{className:`query-panel`,children:e})}function kt({main:e,side:t}){return(0,W.jsxs)(`div`,{className:`split-layout`,children:[(0,W.jsx)(`div`,{className:`split-main`,children:e}),(0,W.jsx)(`aside`,{className:`split-side`,children:t})]})}function G({title:e,text:t,action:n}){return(0,W.jsxs)(`div`,{className:`section-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`h2`,{children:e}),t&&(0,W.jsx)(`p`,{children:t})]}),n&&(0,W.jsx)(`div`,{className:`section-action`,children:n})]})}function K({children:e}){return(0,W.jsxs)(`div`,{className:`alert`,children:[(0,W.jsx)(ee,{size:16}),` `,(0,W.jsx)(`span`,{children:e})]})}function q({children:e,tone:t=`neutral`}){return(0,W.jsx)(`span`,{className:`badge ${t}`,children:e})}function J({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,W.jsxs)(`div`,{className:`metric ${n}`,children:[(0,W.jsx)(`span`,{children:e}),(0,W.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function Y({label:e,value:t,mono:n=!1}){return(0,W.jsxs)(`div`,{className:`summary-item`,children:[(0,W.jsx)(`span`,{children:e}),(0,W.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function At({rows:e}){return(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Command ID`}),(0,W.jsx)(`th`,{children:`Action`}),(0,W.jsx)(`th`,{children:`Actor`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Dry-run`}),(0,W.jsx)(`th`,{children:`Reason`}),(0,W.jsx)(`th`,{children:`Time`})]})}),(0,W.jsxs)(`tbody`,{children:[e.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.ID}),(0,W.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,W.jsx)(`td`,{children:e.Action}),(0,W.jsx)(`td`,{children:e.Actor}),(0,W.jsx)(`td`,{children:e.Status}),(0,W.jsx)(`td`,{children:e.DryRun?`Yes`:`No`}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,W.jsx)(`td`,{children:U(e.CreatedAt)})]},e.ID)),e.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})})}function jt({colSpan:e}){return(0,W.jsx)(`tr`,{children:(0,W.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:`No results`})})}function X({label:e}){return(0,W.jsx)(`section`,{className:`surface`,children:(0,W.jsx)(`div`,{className:`loading-line`,children:e})})}function Mt({value:e}){return(0,W.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function Nt({username:e,collectibles:t}){let n=H(e??``),r=t??[];return r.length===0?(0,W.jsx)(W.Fragment,{children:n||`-`}):(0,W.jsxs)(W.Fragment,{children:[n,(0,W.jsx)(`ul`,{className:`username-branch`,children:r.map(e=>(0,W.jsxs)(`li`,{className:e.Active?``:`inactive`,children:[(0,W.jsx)(`span`,{children:H(e.Username)}),!e.Active&&(0,W.jsx)(`em`,{children:`inactive`})]},e.Username))})]})}var Pt=`verification.review`,Ft=`botverification.review`,It=`botverification.manage`,Lt=(0,g.createContext)({permissions:[],hideThirdPartyVerification:!0});function Rt({permissions:e,hideThirdPartyVerification:t=!0,children:n}){let r=(0,g.useMemo)(()=>({permissions:e,hideThirdPartyVerification:t}),[e,t]);return(0,W.jsx)(Lt.Provider,{value:r,children:n})}function zt(){let{permissions:e}=(0,g.useContext)(Lt);return(0,g.useMemo)(()=>({permissions:e,can:t=>e.includes(`*`)||e.includes(t)}),[e])}function Bt(e){return zt().can(e)}function Vt(){return(0,g.useContext)(Lt).hideThirdPartyVerification}function Ht({permission:e,children:t}){let{can:n}=zt();return n(e)?(0,W.jsx)(W.Fragment,{children:t}):(0,W.jsx)(Ut,{permission:e})}function Ut({permission:e}){return(0,W.jsxs)(Dt,{title:`Not enough rights`,eyebrow:`Console / Access`,children:[(0,W.jsx)(K,{children:`This session was not granted the ${e} permission, so the section stays closed.`}),(0,W.jsx)(`section`,{className:`section-block`,children:(0,W.jsx)(`div`,{className:`entity-head`,children:(0,W.jsxs)(`div`,{children:[(0,W.jsxs)(`div`,{className:`entity-title`,children:[(0,W.jsx)(Qe,{size:16}),` `,`Section unavailable`]}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`Ask an operator to add the permission to TELESRV_ADMIN_UI_PERMISSIONS and sign in again.`})]})})})]})}function Wt({children:e}){return Vt()?(0,W.jsxs)(Dt,{title:`Feature hidden`,eyebrow:`Console / Third-party marks`,children:[(0,W.jsx)(K,{children:`Third-party bot verification is hidden on this server (TELESRV_HIDE_THIRD_PARTY_VERIFICATION=true).`}),(0,W.jsx)(`section`,{className:`section-block`,children:(0,W.jsx)(`div`,{className:`entity-head`,children:(0,W.jsxs)(`div`,{children:[(0,W.jsxs)(`div`,{className:`entity-title`,children:[(0,W.jsx)(Qe,{size:16}),` `,`Not fully finished`]}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`This feature may cause unstable server behavior and is hidden by default. Set TELESRV_HIDE_THIRD_PARTY_VERIFICATION=false to re-enable it.`})]})})})]}):(0,W.jsx)(W.Fragment,{children:e})}function Gt(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function Kt(e){return e.startsWith(`/bot-verification`)?`Third-party verification`:e.startsWith(`/verification`)?`Official Verification`:e.startsWith(`/collectible-usernames`)?`Collectible Usernames`:e.startsWith(`/reserved-usernames`)?`Reserved Usernames`:e.startsWith(`/storage`)?`Storage`:e.startsWith(`/accounts/shared-devices`)?`Shared Devices`:e.startsWith(`/accounts`)?`Accounts`:e.startsWith(`/channels`)?`Supergroups and Channels`:e.startsWith(`/bots`)?`Bots`:e.startsWith(`/moderation`)?`Reports and Moderation`:e.startsWith(`/broadcasts`)?`Broadcasts`:e.startsWith(`/emoji`)?`Emoji`:e.startsWith(`/messages`)?`Message Audit`:e.startsWith(`/stickers`)?`Stickers`:e.startsWith(`/gif-catalog`)?`GIFs`:`Operations Console`}var qt=`telesrv.admin.theme`,Jt=(0,g.createContext)(null);function Yt(e){document.documentElement.setAttribute(`data-theme`,e),document.documentElement.style.colorScheme=e}function Xt({children:e}){let[t,n]=(0,g.useState)(()=>$t());(0,g.useEffect)(()=>{Yt(t);try{localStorage.setItem(qt,t)}catch{}},[t]),(0,g.useEffect)(()=>{if(!window.matchMedia)return;let e=window.matchMedia(`(prefers-color-scheme: dark)`),t=e=>{let t=null;try{t=localStorage.getItem(qt)}catch{t=null}t!==`light`&&t!==`dark`&&n(e.matches?`dark`:`light`)};return e.addEventListener(`change`,t),()=>e.removeEventListener(`change`,t)},[]);let r=(0,g.useCallback)(e=>n(e),[]),i=(0,g.useCallback)(()=>n(e=>e===`dark`?`light`:`dark`),[]),a=(0,g.useMemo)(()=>({theme:t,setTheme:r,toggleTheme:i}),[t,r,i]);return(0,W.jsx)(Jt.Provider,{value:a,children:e})}function Zt(){let e=(0,g.useContext)(Jt);if(!e)throw Error(`useTheme must be used inside ThemeProvider`);return e}function Qt(){let{theme:e,toggleTheme:t}=Zt(),n=e===`light`?`Switch to dark theme`:`Switch to light theme`;return(0,W.jsx)(`button`,{className:`theme-toggle`,type:`button`,onClick:t,"aria-label":n,title:n,children:e===`dark`?(0,W.jsx)(rt,{size:16}):(0,W.jsx)(ze,{size:16})})}function $t(){try{let e=localStorage.getItem(qt);if(e===`light`||e===`dark`)return e}catch{}try{if(window.matchMedia&&window.matchMedia(`(prefers-color-scheme: dark)`).matches)return`dark`}catch{}return`light`}function en({href:e,navigate:t,className:n,children:r}){return(0,W.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function tn(){return(0,W.jsxs)(`div`,{className:`boot-screen`,children:[(0,W.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:(0,W.jsx)(`img`,{src:`/logo.png`,alt:`OwpenGram`})}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`OwpenGram`}),(0,W.jsx)(`small`,{children:`Admin Console`})]})]}),(0,W.jsx)(`div`,{className:`loader-bar`})]})}function nn({actor:e,route:t,navigate:n,onLogout:r,children:i}){let a=Bt(Pt),o=Bt(Ft),s=Vt(),c=t.path.startsWith(`/messages`),[l,u]=(0,g.useState)(c);(0,g.useEffect)(()=>{c&&u(!0)},[c]);async function d(){await k.logout().catch(()=>void 0),r()}return(0,W.jsxs)(`div`,{className:`shell`,children:[(0,W.jsxs)(`aside`,{className:`sidebar`,children:[(0,W.jsxs)(en,{className:`brand`,href:`/`,navigate:n,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:(0,W.jsx)(`img`,{src:`/logo.png`,alt:`OwpenGram`})}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`OwpenGram`}),(0,W.jsx)(`small`,{children:`Admin Console`})]})]}),(0,W.jsx)(`div`,{className:`sidebar-label`,children:`Navigation`}),(0,W.jsxs)(`nav`,{className:`nav-list`,"aria-label":`Primary navigation`,children:[(0,W.jsx)(rn,{icon:(0,W.jsx)(je,{size:16}),href:`/`,route:t,navigate:n,children:`Overview`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(ct,{size:16}),href:`/accounts`,route:t,navigate:n,children:`Accounts`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(Ze,{size:16}),href:`/channels`,route:t,navigate:n,children:`Supergroups / Channels`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(R,{size:16}),href:`/bots`,route:t,navigate:n,children:`Bots`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(Xe,{size:16}),href:`/moderation`,route:t,navigate:n,children:`Reports / Moderation`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(Fe,{size:16}),href:`/broadcasts`,route:t,navigate:n,children:`Broadcasts`}),a&&(0,W.jsx)(rn,{icon:(0,W.jsx)(F,{size:16}),href:`/verification`,route:t,navigate:n,children:`Verification`}),o&&!s&&(0,W.jsx)(rn,{icon:(0,W.jsx)(tt,{size:16}),href:`/bot-verification`,route:t,navigate:n,children:`Third-party marks`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(ue,{size:16}),href:`/collectible-usernames`,route:t,navigate:n,children:`NFT Usernames`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(de,{size:16}),href:`/reserved-usernames`,route:t,navigate:n,children:`Reserved Usernames`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(be,{size:16}),href:`/storage`,route:t,navigate:n,children:`Storage`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(nt,{size:16}),href:`/stickers`,route:t,navigate:n,children:`Stickers`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(et,{size:16}),href:`/emoji`,route:t,navigate:n,children:`Emoji`}),(0,W.jsx)(rn,{icon:(0,W.jsx)(Ce,{size:16}),href:`/gif-catalog`,route:t,navigate:n,children:`GIFs`}),(0,W.jsxs)(`div`,{className:`nav-section ${c?`active`:``} ${l?`open`:``}`,children:[(0,W.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":l,onClick:()=>u(e=>!e),children:[(0,W.jsx)(Le,{size:16}),(0,W.jsx)(`span`,{children:`Messages`}),(0,W.jsx)(he,{className:`nav-section-chevron`,size:15})]}),l&&(0,W.jsxs)(`div`,{className:`nav-children`,children:[(0,W.jsx)(rn,{href:`/messages/private`,route:t,navigate:n,activeWhen:e=>e===`/messages`||e===`/messages/detail`||e.startsWith(`/messages/private`),children:`Private`}),(0,W.jsx)(rn,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:`Groups`})]})]})]})]}),(0,W.jsxs)(`div`,{className:`workspace`,children:[(0,W.jsxs)(`header`,{className:`topbar`,children:[(0,W.jsx)(`div`,{children:(0,W.jsx)(`h1`,{children:Kt(t.path)})}),(0,W.jsxs)(`div`,{className:`topbar-actions`,children:[(0,W.jsx)(Qt,{}),(0,W.jsx)(`span`,{className:`actor-pill`,children:`Actor: ${e}`}),(0,W.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:d,title:`Log out`,children:[(0,W.jsx)(Ne,{size:16}),` `,`Log out`]})]})]}),(0,W.jsx)(`main`,{className:`content`,children:i})]})]})}function rn({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,W.jsxs)(en,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,W.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,W.jsx)(`span`,{children:i})]})}function an({onLogin:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1);async function s(n){n.preventDefault(),o(!0),i(``);try{let n=await k.login(t);e({actor:n.actor,permissions:n.permissions??[]})}catch(e){i(O(e))}finally{o(!1)}}return(0,W.jsxs)(`main`,{className:`login-page`,children:[(0,W.jsxs)(`div`,{className:`bg-orbs`,"aria-hidden":`true`,children:[(0,W.jsx)(`div`,{className:`bg-orb bg-orb--1`}),(0,W.jsx)(`div`,{className:`bg-orb bg-orb--2`}),(0,W.jsx)(`div`,{className:`bg-orb bg-orb--3`})]}),(0,W.jsxs)(`section`,{className:`login-panel`,children:[(0,W.jsxs)(`div`,{className:`login-head`,children:[(0,W.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,W.jsx)(`span`,{className:`brand-mark`,children:(0,W.jsx)(`img`,{src:`/logo.png`,alt:`OwpenGram`})}),(0,W.jsxs)(`span`,{children:[(0,W.jsx)(`strong`,{children:`OwpenGram`}),(0,W.jsx)(`small`,{children:`Admin Console`})]})]}),(0,W.jsxs)(`div`,{className:`login-head-actions`,children:[(0,W.jsx)(Qt,{}),(0,W.jsx)(`span`,{className:`login-chip`,children:`Local access`})]})]}),(0,W.jsxs)(`div`,{className:`login-copy`,children:[(0,W.jsx)(`h1`,{children:`Operations Admin`}),(0,W.jsx)(`p`,{children:`Enter credentials to open the console.`})]}),r&&(0,W.jsx)(K,{children:r}),(0,W.jsxs)(`form`,{className:`form-stack`,onSubmit:s,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:`Admin password or token`}),(0,W.jsx)(`input`,{autoFocus:!0,type:`password`,value:t,autoComplete:`current-password`,onChange:e=>n(e.target.value)})]}),(0,W.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:a,children:a?`Logging in`:`Log in`})]})]})]})}var on=m();function sn({kind:e,id:t,onClose:n,onDone:r}){let[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``);(0,g.useEffect)(()=>{if(!i){s(``);return}let e=URL.createObjectURL(i);return s(e),()=>URL.revokeObjectURL(e)},[i]);async function m(){if(!i){p(`Choose an image file first.`);return}if(!c.trim()){p(`Please enter an operation reason`);return}d(!0),p(``);try{let a=e===`channel`?`channel_id`:`user_id`,o=new FormData;o.set(`metadata`,JSON.stringify({command_id:``,reason:c.trim(),confirm:!0,[a]:t})),o.set(`file`,i,i.name);let s=e===`channel`?await k.setChannelAvatar(o):await k.setAccountAvatar(o);if(s.error){p(s.error);return}r(),n()}catch(e){p(O(e))}finally{d(!1)}}return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Change avatar`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:e===`channel`?`Channel`:`Account`}),(0,W.jsx)(`h2`,{children:`Change avatar`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:n,disabled:u,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsxs)(`label`,{className:`gift-file-picker ${i?`has-file`:``}`,children:[(0,W.jsx)(`input`,{type:`file`,accept:`image/png,image/jpeg,image/webp`,onChange:e=>a(e.target.files?.[0]??null)}),o?(0,W.jsx)(`img`,{className:`gift-file-icon`,src:o,alt:``,style:{objectFit:`cover`}}):(0,W.jsx)(Ae,{size:22}),(0,W.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,W.jsx)(`span`,{className:`gift-field-label`,children:`New avatar`}),(0,W.jsx)(`strong`,{children:i?i.name:`Choose a JPEG, PNG, or WebP image`})]}),(0,W.jsx)(`span`,{className:`gift-file-action`,children:i?`Change file`:`Choose file`})]}),(0,W.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,W.jsx)(`span`,{children:`Audit reason`}),(0,W.jsx)(`input`,{value:c,placeholder:`Briefly describe why this avatar is being changed`,onChange:e=>l(e.target.value)})]}),f&&(0,W.jsx)(K,{children:f})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:n,disabled:u,children:`Close`}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:m,disabled:u,children:[u?(0,W.jsx)(L,{className:`spin`,size:15}):(0,W.jsx)(ot,{size:15}),`Upload avatar`]})]})]})}),document.body)}function Z({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,disabled:o=!1,onDone:s,onError:c,secretField:l}){let[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(null),[_,v]=(0,g.useState)(``),[y,b]=(0,g.useState)(!1),[x,S]=(0,g.useState)(!1);function C(){p(``),h(null),v(``),S(!1)}async function w(e){if(!f.trim()){v(`Please enter an operation reason`);return}b(!0),v(``);try{let r={...n(),reason:f,confirm:e};h(await k.action(t,r)),e&&s?.()}catch(e){v(c?.(e)||O(e))}finally{b(!1)}}let T=m?.dry_run&&!m.error,E=`btn ${a===`danger`?`danger`:a===`warn`?`warn`:``} ${i?`compact-btn`:``}`,D=(0,g.useMemo)(()=>{try{return n()}catch(e){return{payload_error:O(e)}}},[u,n]),A=l&&m?.details&&typeof m.details[l]==`string`?m.details[l]:``,j=A&&m?.details?Object.fromEntries(Object.entries(m.details).filter(([e])=>e!==l)):m?.details;async function M(){await navigator.clipboard.writeText(A),S(!0)}return(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:E,type:`button`,disabled:o,onClick:()=>{C(),d(!0)},children:[r,e]}),u&&(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`Action Flow`}),(0,W.jsx)(`h2`,{children:e})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>d(!1),"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsxs)(`div`,{className:`command-steps`,children:[(0,W.jsxs)(`div`,{className:`command-step ${f.trim()?`done`:`active`}`,children:[(0,W.jsx)(`span`,{children:`1`}),(0,W.jsx)(`strong`,{children:`Enter reason`})]}),(0,W.jsxs)(`div`,{className:`command-step ${m?.dry_run?`done`:f.trim()?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`2`}),(0,W.jsx)(`strong`,{children:`Dry-run check`})]}),(0,W.jsxs)(`div`,{className:`command-step ${m&&!m.dry_run&&!m.error?`done`:T?`active`:``}`,children:[(0,W.jsx)(`span`,{children:`3`}),(0,W.jsx)(`strong`,{children:`Confirm execution`})]})]}),(0,W.jsxs)(`label`,{className:`form-field`,children:[(0,W.jsx)(`span`,{children:`Operation reason`}),(0,W.jsx)(`textarea`,{value:f,onChange:e=>p(e.target.value),rows:3,placeholder:`Describe why this operation is being performed`})]}),(0,W.jsxs)(`div`,{className:`command-preview`,children:[(0,W.jsxs)(`div`,{className:`preview-head`,children:[(0,W.jsx)(z,{size:14}),` `,`Request preview`]}),(0,W.jsx)(Mt,{value:JSON.stringify(D,null,2)})]}),_&&(0,W.jsx)(K,{children:_}),m&&(0,W.jsxs)(`div`,{className:`result-box`,children:[(0,W.jsxs)(`div`,{className:`result-title`,children:[m.error?(0,W.jsx)(ee,{size:16}):(0,W.jsx)(I,{size:16}),(0,W.jsx)(`strong`,{children:m.message||m.error||`Action result`})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:`Command ID`}),(0,W.jsx)(`strong`,{children:m.command_id})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:`Status`}),(0,W.jsx)(`strong`,{children:m.status})]}),(0,W.jsxs)(`div`,{className:`result-line`,children:[(0,W.jsx)(`span`,{children:`Dry-run`}),(0,W.jsx)(`strong`,{children:m.dry_run?`Yes`:`No`})]}),(0,W.jsx)(`div`,{className:`result-message`,children:m.message||m.error}),A&&(0,W.jsxs)(`div`,{className:`secret-reveal`,children:[(0,W.jsx)(`div`,{className:`secret-reveal-label`,children:`One-time secret — copy it now, it won't be shown again`}),(0,W.jsxs)(`div`,{className:`secret-reveal-row`,children:[(0,W.jsx)(`code`,{className:`secret-reveal-value`,children:`•`.repeat(Math.min(A.length,40))}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void M(),children:[x?(0,W.jsx)(me,{size:15}):(0,W.jsx)(ve,{size:15}),x?`Copied`:`Copy`]})]})]}),j&&Object.keys(j).length>0&&(0,W.jsx)(Mt,{value:JSON.stringify(j,null,2)})]})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>d(!1),children:`Close`}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>w(!1),disabled:y,children:[y?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(He,{size:15}),m?`Run dry-run again`:`Run dry-run first`]}),(0,W.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>w(!0),disabled:y||!T,children:[(0,W.jsx)(I,{size:15}),`Confirm execution`]})]})]})}),document.body)]})}var cn=[[`#FF885E`,`#FF516A`],[`#FFCD6A`,`#FFA85C`],[`#82B1FF`,`#665FFF`],[`#A0DE7E`,`#54CB68`],[`#53EDD6`,`#28C9B7`],[`#72D5FD`,`#2A9EF1`],[`#E0A2F3`,`#D669ED`]];function ln(e){return cn[Math.abs(e)%cn.length]}function un(e){let t=Array.from(e);return t.length>0?t[0]:``}function dn(e,t,n){let r=`${e} ${t}`.trim().split(/\s+/).filter(Boolean),i=r.length>0?r:n?[n]:[];if(i.length===0)return`T`;let a=un(i[0]);return i.length>1&&(a+=un(i[i.length-1])),a.toUpperCase()}function fn({id:e,kind:t=`user`,firstName:n=``,lastName:r=``,username:i=``,title:a=``,size:o=34,refreshKey:s}){let[c,l]=(0,g.useState)(!1);if((0,g.useEffect)(()=>{l(!1)},[e,t,s]),c){let[s,c]=ln(e);return(0,W.jsx)(`div`,{className:`avatar-fallback`,style:{width:o,height:o,background:`linear-gradient(135deg, ${s}, ${c})`,fontSize:Math.round(o*.42)},children:t===`channel`?dn(a,``,i):dn(n,r,i)})}return(0,W.jsx)(`img`,{className:`avatar-photo-img`,src:`${t===`channel`?`/api/channels/${e}/avatar`:`/api/accounts/${e}/avatar`}${s===void 0?``:`?v=${encodeURIComponent(String(s))}`}`,alt:``,loading:`lazy`,style:{width:o,height:o},onError:()=>l(!0)})}function pn({rows:e,userID:t,onDone:n}){let[r,i]=(0,g.useState)(()=>new Set);(0,g.useEffect)(()=>{i(new Set)},[t]);let a=(0,g.useMemo)(()=>e.filter(e=>!r.has(e.Hash)),[e,r]);function o(e){i(t=>e(t)),n()}return(0,W.jsxs)(`div`,{className:`authorization-block`,children:[(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Device`}),(0,W.jsx)(`th`,{children:`Platform`}),(0,W.jsx)(`th`,{children:`IP`}),(0,W.jsx)(`th`,{children:`Last active`}),(0,W.jsx)(`th`,{className:`device-actions-head`,children:`Actions`})]})}),(0,W.jsxs)(`tbody`,{children:[a.map(n=>(0,W.jsxs)(`tr`,{children:[(0,W.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,W.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,W.jsx)(`td`,{children:n.IP}),(0,W.jsx)(`td`,{children:U(n.ActiveAt)}),(0,W.jsx)(`td`,{className:`device-actions-cell`,children:(0,W.jsxs)(`div`,{className:`device-actions`,children:[(0,W.jsx)(Z,{label:`Revoke current`,icon:(0,W.jsx)(Ne,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,hash:n.Hash}),onDone:()=>o(e=>new Set([...e,n.Hash]))}),(0,W.jsx)(Z,{label:`Keep current`,icon:(0,W.jsx)(Ze,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,keep_hash:n.Hash}),onDone:()=>o(()=>new Set(e.filter(e=>e.Hash!==n.Hash).map(e=>e.Hash)))})]})})]},n.Hash)),a.length===0&&(0,W.jsx)(jt,{colSpan:5})]})]})}),(0,W.jsx)(`div`,{className:`danger-zone`,children:(0,W.jsx)(Z,{label:`Revoke all devices`,icon:(0,W.jsx)(pe,{size:15}),path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,revoke_all:!0}),onDone:()=>o(()=>new Set(e.map(e=>e.Hash)))})})]})}function mn({scam:e,fake:t}){return!e&&!t?null:(0,W.jsxs)(W.Fragment,{children:[e&&(0,W.jsx)(q,{tone:`danger`,children:`SCAM`}),t&&(0,W.jsx)(q,{tone:`danger`,children:`FAKE`})]})}function hn({idKey:e,id:t,path:n,scam:r,fake:i,onDone:a}){return(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:r?`Clear SCAM`:`Mark as SCAM`,icon:(0,W.jsx)(Xe,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,scam:!r,fake:r?i:!1}),onDone:a}),(0,W.jsx)(Z,{label:i?`Clear FAKE`:`Mark as FAKE`,icon:(0,W.jsx)(ne,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,fake:!i,scam:i?r:!1}),onDone:a})]})}function gn({id:e,support:t,onDone:n}){return(0,W.jsx)(Z,{label:t?`Clear support`:`Mark as support`,icon:(0,W.jsx)(Me,{size:15}),tone:`neutral`,path:`/api/actions/set-support`,payload:()=>({user_id:e,support:!t}),onDone:n})}function _n({idKey:e,id:t,path:n,current:r,onDone:i}){let[a,o]=(0,g.useState)(r.replace(/^@/,``));return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Username`}),(0,W.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`username`})]}),(0,W.jsx)(Z,{label:`Set username`,icon:(0,W.jsx)(ue,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,username:a.trim().replace(/^@/,``)}),onDone:i})]})}function vn({id:e,path:t,currentFirstName:n,currentLastName:r,onDone:i}){let[a,o]=(0,g.useState)(n),[s,c]=(0,g.useState)(r);return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`First name`}),(0,W.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`First name`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Last name`}),(0,W.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`Last name`})]}),(0,W.jsx)(Z,{label:`Set name`,icon:(0,W.jsx)(ae,{size:15}),tone:`neutral`,path:t,payload:()=>({user_id:e,first_name:a.trim(),last_name:s.trim()}),onDone:i})]})}function yn({id:e,path:t,current:n,onDone:r}){let[i,a]=(0,g.useState)(n);return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Phone number`}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`15551234567`})]}),(0,W.jsx)(Z,{label:`Set phone`,icon:(0,W.jsx)(Ve,{size:15}),tone:`warn`,path:t,payload:()=>({user_id:e,phone:i.trim()}),onDone:r})]})}function bn({id:e,path:t,current:n,onDone:r}){let[i,a]=(0,g.useState)(n);return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Login email`}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`name@example.com (empty clears it)`,type:`email`})]}),(0,W.jsx)(Z,{label:i.trim()?`Set login email`:`Clear login email`,icon:(0,W.jsx)(Pe,{size:15}),tone:`warn`,path:t,payload:()=>({user_id:e,email:i.trim()}),onDone:r})]})}function xn({idKey:e,id:t,path:n,onDone:r}){let[i,a]=(0,g.useState)(!1),[o,s]=(0,g.useState)(!0),[c,l]=(0,g.useState)(`0`),[u,d]=(0,g.useState)(``);return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:i,onChange:e=>a(e.target.checked)}),` `,`Profile color`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:o,onChange:e=>s(e.target.checked)}),` `,`Enable color`]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Color index`}),(0,W.jsx)(`input`,{type:`number`,min:`0`,max:`20`,value:c,onChange:e=>l(e.target.value)})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Background emoji ID`}),(0,W.jsx)(`input`,{value:u,onChange:e=>d(e.target.value),placeholder:`0`})]}),(0,W.jsx)(Z,{label:`Set color`,icon:(0,W.jsx)(Be,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,for_profile:i,has_color:o,color:gt(c),background_emoji_id:u.trim()||`0`}),onDone:r})]})}function Sn({idKey:e,id:t,path:n,onDone:r}){let[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(`0`);return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Emoji document ID`}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`0 = clear`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Until (unix, 0 = permanent)`}),(0,W.jsx)(`input`,{type:`number`,min:`0`,value:o,onChange:e=>s(e.target.value)})]}),(0,W.jsx)(Z,{label:`Set emoji status`,icon:(0,W.jsx)(et,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,document_id:i.trim()||`0`,until:gt(o)}),onDone:r})]})}function Cn({channel:e,onDone:t}){let[n,r]=(0,g.useState)(e.Gigagroup),[i,a]=(0,g.useState)(e.AntiSpam),[o,s]=(0,g.useState)(e.ParticipantsHidden),[c,l]=(0,g.useState)(e.NoForwards),[u,d]=(0,g.useState)(e.JoinToSend),[f,p]=(0,g.useState)(e.JoinRequest),[m,h]=(0,g.useState)(String(e.SlowmodeSeconds));(0,g.useEffect)(()=>{r(e.Gigagroup),a(e.AntiSpam),s(e.ParticipantsHidden),l(e.NoForwards),d(e.JoinToSend),p(e.JoinRequest),h(String(e.SlowmodeSeconds))},[e]);function _(){let t={channel_id:e.ID};return n!==e.Gigagroup&&(t.gigagroup=n),i!==e.AntiSpam&&(t.antispam=i),o!==e.ParticipantsHidden&&(t.participants_hidden=o),c!==e.NoForwards&&(t.noforwards=c),u!==e.JoinToSend&&(t.join_to_send=u),f!==e.JoinRequest&&(t.join_request=f),gt(m)!==e.SlowmodeSeconds&&(t.slowmode_seconds=gt(m)),t}return(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:n,onChange:e=>r(e.target.checked)}),` `,`Gigagroup`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:i,onChange:e=>a(e.target.checked)}),` `,`Aggressive anti-spam`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:o,onChange:e=>s(e.target.checked)}),` `,`Hide members`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:c,onChange:e=>l(e.target.checked)}),` `,`Restrict forwarding`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:u,onChange:e=>d(e.target.checked)}),` `,`Join to send messages`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:f,onChange:e=>p(e.target.checked)}),` `,`Join by request`]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Slowmode (seconds)`}),(0,W.jsx)(`input`,{type:`number`,min:`0`,max:`86400`,value:m,onChange:e=>h(e.target.value)})]}),(0,W.jsx)(Z,{label:`Apply settings`,icon:(0,W.jsx)(Ye,{size:15}),tone:`warn`,path:`/api/actions/set-channel-settings`,payload:_,onDone:t})]})}function wn({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(`1`),[f,p]=(0,g.useState)(()=>Tn(new Date(Date.now()+7*864e5))),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(!1),[y,b]=(0,g.useState)(0);async function x(){s(!0),a(``);try{let t=await k.account(e);r(t),t.Restriction.Frozen&&(t.Restriction.Until&&p(Tn(new Date(t.Restriction.Until))),h(t.Restriction.AppealURL||``))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{x(),l(`profile`)},[e]),i)return(0,W.jsx)(K,{children:i});if(!n)return(0,W.jsx)(X,{label:o?`Loading account detail`:`Waiting for data`});let S=n.Account,C=[{key:`profile`,label:`Profile & Status`,icon:(0,W.jsx)(ae,{size:15})},{key:`devices`,label:`Authorized Devices`,icon:(0,W.jsx)(Re,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,W.jsx)(Ye,{size:15})}];return(0,W.jsxs)(Dt,{title:`Account #${S.ID}`,eyebrow:`Account Profile`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,W.jsx)(le,{size:15}),` `,`Back to list`]}),children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{className:`entity-head-main`,children:[(0,W.jsxs)(`div`,{className:`avatar-edit-slot`,children:[(0,W.jsx)(fn,{id:S.ID,firstName:S.FirstName,lastName:S.LastName,username:S.Username,size:64,refreshKey:y||void 0}),(0,W.jsx)(`button`,{className:`icon-btn avatar-edit-btn`,type:`button`,"aria-label":`Change avatar`,title:`Change avatar`,onClick:()=>v(!0),children:(0,W.jsx)(Ae,{size:13})})]}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:ft(S)}),(0,W.jsxs)(`div`,{className:`entity-subtitle`,children:[H(S.Username)||`No username`,` · `,dt(S.Phone)||`No phone`]}),S.Collectibles?.length>0&&(0,W.jsx)(`div`,{className:`entity-subtitle`,children:(0,W.jsx)(Nt,{username:``,collectibles:S.Collectibles})})]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[S.PremiumUntil>0?(0,W.jsx)(q,{tone:`good`,children:`Premium`}):(0,W.jsx)(q,{children:`Not premium`}),n.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Not verified`}),(0,W.jsx)(mn,{scam:n.Scam,fake:n.Fake}),S.Frozen?(0,W.jsx)(q,{tone:`danger`,children:`Account frozen`}):(0,W.jsx)(q,{children:`Account active`})]})]}),(0,W.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Account sections`,children:C.map(e=>(0,W.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`User ID`,value:String(S.ID),mono:!0}),(0,W.jsx)(Y,{label:`Last active`,value:mt(n.LastSeenAt)||`-`}),(0,W.jsx)(Y,{label:`Premium expires`,value:S.PremiumUntil>0?mt(S.PremiumUntil):`None`}),(0,W.jsx)(Y,{label:`Updated`,value:U(S.UpdatedAt)||`-`}),(0,W.jsx)(Y,{label:`Authorized devices`,value:String(n.Authorizations.length)}),(0,W.jsx)(Y,{label:`Account flags`,value:`support=${n.Support} bot=${n.Bot}`}),(0,W.jsx)(Y,{label:`Restriction`,value:n.HasRestriction?n.Restriction.Reason||`Restricted`:`None`}),(0,W.jsx)(Y,{label:`Frozen since`,value:n.Restriction.Since?U(n.Restriction.Since):`None`}),(0,W.jsx)(Y,{label:`Appeal deadline`,value:n.Restriction.Until?U(n.Restriction.Until):`None`}),(0,W.jsx)(Y,{label:`Appeal URL`,value:n.Restriction.AppealURL||`None`}),(0,W.jsx)(Y,{label:`Created`,value:U(S.CreatedAt)||`-`})]}),n.About&&(0,W.jsx)(`p`,{className:`about-text`,children:n.About})]}),c===`devices`&&(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Authorized Devices`,text:`${n.Authorizations.length} authorizations`}),(0,W.jsx)(pn,{rows:n.Authorizations,userID:S.ID,onDone:x})]}),c===`actions`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`action-groups`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Freeze & Restriction`,text:`Blocks sign-in and marks the account for appeal review.`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Appeal deadline`}),(0,W.jsx)(`input`,{"aria-label":`Freeze appeal deadline`,value:f,onChange:e=>p(e.target.value),type:`datetime-local`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Appeal URL`}),(0,W.jsx)(`input`,{"aria-label":`Freeze appeal URL`,value:m,onChange:e=>h(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,W.jsx)(Z,{label:S.Frozen?`Update freeze`:`Freeze account`,icon:(0,W.jsx)(ee,{size:15}),tone:`danger`,path:`/api/actions/set-frozen`,payload:()=>({user_id:S.ID,frozen:!0,freeze_until:new Date(f).toISOString(),freeze_appeal_url:m.trim()}),onDone:x}),S.Frozen&&(0,W.jsx)(Z,{label:`Unfreeze account`,icon:(0,W.jsx)(ee,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:S.ID,frozen:!1}),onDone:x})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Premium`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`attr-block`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Premium duration (months)`}),(0,W.jsx)(`input`,{"aria-label":`Set premium duration in months`,value:u,onChange:e=>d(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:`Set premium`,icon:(0,W.jsx)(re,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:S.ID,months:gt(u)}),onDone:x}),(0,W.jsx)(Z,{label:`Clear premium`,icon:(0,W.jsx)(re,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:S.ID,months:0}),onDone:x})]})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Verification & Moderation Flags`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:n.Verified?`Clear verified`:`Set verified`,icon:(0,W.jsx)(F,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:S.ID,verified:!n.Verified}),onDone:x}),(0,W.jsx)(hn,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-flags`,scam:n.Scam,fake:n.Fake,onDone:x}),(0,W.jsx)(gn,{id:S.ID,support:n.Support,onDone:x})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Username`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(_n,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-username`,current:S.Username,onDone:x})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Name`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(vn,{id:S.ID,path:`/api/actions/set-account-profile`,currentFirstName:S.FirstName,currentLastName:S.LastName,onDone:x})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Phone Number`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(yn,{id:S.ID,path:`/api/actions/set-account-phone`,current:S.Phone,onDone:x})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Login Email`,text:`The email used for sign-in / password-recovery, not a contact address.`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(bn,{id:S.ID,path:`/api/actions/set-account-login-email`,current:S.LoginEmail,onDone:x})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Profile Color`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(xn,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-color`,onDone:x})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Emoji Status`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(Sn,{idKey:`user_id`,id:S.ID,path:`/api/actions/set-account-emoji-status`,onDone:x})})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Recent Admin Actions`,text:`Last 30 audit rows`,action:(0,W.jsx)(qe,{size:16})}),(0,W.jsx)(At,{rows:n.AuditLogs})]})]}),_&&(0,W.jsx)(sn,{kind:`user`,id:S.ID,onClose:()=>v(!1),onDone:()=>{b(e=>e+1),x()}})]})}function Tn(e){return new Date(e.getTime()-e.getTimezoneOffset()*6e4).toISOString().slice(0,16)}function En(e){return e.reduce((e,t)=>(e.devices+=t.DeviceCount,e),{devices:0})}function Dn(e){return e.reduce((e,t)=>(t.Megagroup&&(e.megagroups+=1),t.Broadcast&&(e.broadcasts+=1),t.Verified&&(e.verified+=1),e),{megagroups:0,broadcasts:0,verified:0})}var On={beforeID:0,beforeActiveUS:0};function kn({navigate:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(50),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)([]),[d,f]=(0,g.useState)(On),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(``);async function v(e,t){m(!0),_(``);let n=new URLSearchParams({limit:String(r)});e.trim()&&n.set(`q`,e.trim()),(t.beforeID||t.beforeActiveUS)&&(n.set(`before_id`,String(t.beforeID)),n.set(`before_active_us`,String(t.beforeActiveUS)));try{let e=await k.accounts(n);return o(e),e}catch(e){return _(O(e)),null}finally{m(!1)}}async function y(){u([]),f(On),await v(t,On)}async function b(){if(!a?.has_more)return;let e={beforeID:a.next_before_id,beforeActiveUS:a.next_before_active_us};await v(t,e)&&(u(e=>[...e,d]),f(e))}async function x(){if(l.length===0)return;let e=l[l.length-1];await v(t,e)&&(u(e=>e.slice(0,-1)),f(e))}async function S(){try{c(await k.accountStats())}catch{}}(0,g.useEffect)(()=>{y(),S()},[]);let C=En(a?.rows??[]),w=l.length>0&&!p,T=!!a?.has_more&&!p;return(0,W.jsxs)(Dt,{title:`Accounts`,eyebrow:a?.listing===!1?`Search results`:`Recently active accounts`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>e(`/accounts/shared-devices`),children:[(0,W.jsx)($e,{size:15}),` `,`Shared devices`]}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>{y(),S()},disabled:p,children:[(0,W.jsx)(Ke,{size:15}),` `,`Refresh`]})]}),children:[h&&(0,W.jsx)(K,{children:h}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Total users`,value:s?String(s.total):`…`}),(0,W.jsx)(J,{label:`Online now`,value:s?String(s.online):`…`,tone:`good`}),(0,W.jsx)(J,{label:`Online device records`,value:String(C.devices)})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),y()},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:`User ID / phone / username / email / name`})]}),(0,W.jsxs)(`label`,{className:`gift-page-size`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsxs)(`select`,{value:String(r),onChange:e=>i(Number(e.target.value)),children:[(0,W.jsx)(`option`,{value:`10`,children:`10`}),(0,W.jsx)(`option`,{value:`20`,children:`20`}),(0,W.jsx)(`option`,{value:`50`,children:`50`}),(0,W.jsx)(`option`,{value:`100`,children:`100`})]})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:p,children:[p?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,`Search`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void x(),disabled:!w,children:[(0,W.jsx)(ge,{size:15}),` `,`Previous page`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void b(),disabled:!T,children:[(0,W.jsx)(_e,{size:15}),` `,`Next page`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{className:`avatar-col`}),(0,W.jsx)(`th`,{children:`User ID`}),(0,W.jsx)(`th`,{children:`Phone`}),(0,W.jsx)(`th`,{children:`Username`}),(0,W.jsx)(`th`,{children:`Name`}),(0,W.jsx)(`th`,{children:`Login email`}),(0,W.jsx)(`th`,{children:`Device`}),(0,W.jsx)(`th`,{children:`Last active`}),(0,W.jsx)(`th`,{children:`Premium`}),(0,W.jsx)(`th`,{children:`Verified`}),(0,W.jsx)(`th`,{children:`Frozen`}),(0,W.jsx)(`th`,{children:`Updated`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[a?.rows.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`avatar-col`,children:(0,W.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/accounts/${t.ID}`),"aria-label":`Open account ${t.ID}`,children:(0,W.jsx)(fn,{id:t.ID,firstName:t.FirstName,lastName:t.LastName,username:t.Username})})}),(0,W.jsx)(`td`,{className:`mono`,children:t.ID}),(0,W.jsx)(`td`,{children:dt(t.Phone)}),(0,W.jsx)(`td`,{children:(0,W.jsx)(Nt,{username:t.Username,collectibles:t.Collectibles})}),(0,W.jsx)(`td`,{children:ft(t)}),(0,W.jsx)(`td`,{children:t.LoginEmail||(0,W.jsx)(`span`,{className:`muted-cell`,children:`None`})}),(0,W.jsx)(`td`,{children:t.DeviceCount}),(0,W.jsx)(`td`,{children:U(t.LastActiveAt)}),(0,W.jsx)(`td`,{children:t.PremiumUntil>0?(0,W.jsxs)(q,{tone:`good`,children:[`Premium`,` `,mt(t.PremiumUntil)]}):(0,W.jsx)(q,{children:`None`})}),(0,W.jsxs)(`td`,{children:[t.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Not verified`}),` `,(0,W.jsx)(mn,{scam:t.Scam,fake:t.Fake})]}),(0,W.jsx)(`td`,{children:t.Frozen?(0,W.jsx)(q,{tone:`danger`,children:`Frozen`}):(0,W.jsx)(q,{children:`Normal`})}),(0,W.jsx)(`td`,{children:U(t.UpdatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${t.ID}`),children:[`Details`,` `,(0,W.jsx)(_e,{size:14})]})})]},t.ID)),(!a||a.rows.length===0)&&(0,W.jsx)(jt,{colSpan:12})]})]})})]})}function An({navigate:e}){let[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(!1),[a,o]=(0,g.useState)(0),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(e=!1){c(!0),u(``);let t=new URLSearchParams({limit:`20`,offset:String(e?a:0)});try{let r=await k.sharedDeviceGroups(t),a=r.rows??[];n(t=>e?[...t,...a]:a),o(r.next_offset),i(!!r.has_more)}catch(e){u(O(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d(!1)},[]);let f=t.reduce((e,t)=>e+t.AccountCount,0);return(0,W.jsxs)(Dt,{title:`Shared Devices`,eyebrow:`Multi-account signal — device/IP overlap across different accounts`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>e(`/accounts`),children:[(0,W.jsx)(le,{size:15}),` `,`Back to accounts`]}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>d(!1),disabled:s,children:[(0,W.jsx)(Ke,{size:15,className:s?`spin`:``}),` `,`Refresh`]})]}),children:[l&&(0,W.jsx)(K,{children:l}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Device groups on page`,value:String(t.length)}),(0,W.jsx)(J,{label:`Accounts flagged on page`,value:String(f),tone:`warn`})]}),(0,W.jsxs)(`p`,{className:`about-text`,children:[`Each card below is a device fingerprint (device model + OS + platform + IP) that more than one account has authorized from. `,`device_model/system_version are self-reported by the client, and IP alone can collide innocently -- use this as a lead, not a verdict.`]}),(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[t.map(t=>(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:t.DeviceModel||`Unknown device`,text:`${t.Platform||`unknown platform`} ${t.SystemVersion} · ${t.IP} · last active ${U(t.LastActiveAt)}`,action:(0,W.jsxs)(q,{tone:`warn`,children:[(0,W.jsx)($e,{size:12}),` `,`${t.AccountCount} accounts`]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{className:`avatar-col`}),(0,W.jsx)(`th`,{children:`User ID`}),(0,W.jsx)(`th`,{children:`Phone`}),(0,W.jsx)(`th`,{children:`Username`}),(0,W.jsx)(`th`,{children:`Name`}),(0,W.jsx)(`th`,{children:`Active from this device`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsx)(`tbody`,{children:t.Accounts.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`avatar-col`,children:(0,W.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/accounts/${t.UserID}`),"aria-label":`Open account ${t.UserID}`,children:(0,W.jsx)(fn,{id:t.UserID,firstName:t.FirstName,lastName:t.LastName,username:t.Username})})}),(0,W.jsx)(`td`,{className:`mono`,children:t.UserID}),(0,W.jsx)(`td`,{children:dt(t.Phone)}),(0,W.jsx)(`td`,{children:H(t.Username)||`-`}),(0,W.jsx)(`td`,{children:ft(t)||`-`}),(0,W.jsx)(`td`,{children:U(t.ActiveAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${t.UserID}`),children:[`Details`,` `,(0,W.jsx)(_e,{size:14})]})})]},t.UserID))})]})})]},`${t.DeviceModel}|${t.SystemVersion}|${t.Platform}|${t.IP}`)),t.length===0&&(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsx)(`table`,{className:`data-table`,children:(0,W.jsx)(`tbody`,{children:(0,W.jsx)(jt,{colSpan:7})})})})]}),r&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>d(!0),disabled:s,children:[s?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(he,{size:15}),` `,`Load more`]})})]})}function jn({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim());try{o((await k.accounts(e)).rows)}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{d()},[]),(0,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,W.jsx)(ut,{size:13}),` `,`Clear`]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(me,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:ft(t)}),(0,W.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,W.jsx)(`span`,{children:H(t.Username)||dt(t.Phone)||`-`})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Search user_id / phone / username`}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,W.jsx)(L,{size:14,className:`spin`}):`Search`})]}),l&&(0,W.jsx)(`div`,{className:`picker-error`,children:l}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>(0,W.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:ft(e)}),(0,W.jsx)(`span`,{children:H(e.Username)||dt(e.Phone)||`-`}),e.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Regular`})]},e.ID)),a.length===0&&!s?(0,W.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function Mn({label:e,selected:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim());try{o((await k.accounts(e)).rows)}catch(e){u(O(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d()},[]);function f(e){t.some(t=>t.ID===e.ID)?n(t.filter(t=>t.ID!==e.ID)):n([...t,e])}function p(e){n(t.filter(t=>t.ID!==e))}return(0,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t.length>0?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n([]),children:[(0,W.jsx)(ut,{size:13}),` `,`Clear all`]}):null]}),t.length>0?(0,W.jsx)(`div`,{className:`picker-chip-list`,children:t.map(e=>(0,W.jsxs)(`span`,{className:`picker-chip`,children:[ft(e),` `,(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`button`,{type:`button`,onClick:()=>p(e.ID),"aria-label":`Remove ${e.ID}`,children:(0,W.jsx)(ut,{size:12})})]},e.ID))}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Search user_id / phone / username`}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,W.jsx)(L,{size:14,className:`spin`}):`Search`})]}),l&&(0,W.jsx)(`div`,{className:`picker-error`,children:l}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>{let n=t.some(t=>t.ID===e.ID);return(0,W.jsxs)(`button`,{className:`picker-row ${n?`selected`:``}`,type:`button`,onClick:()=>f(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:ft(e)}),(0,W.jsx)(`span`,{children:H(e.Username)||dt(e.Phone)||`-`}),n?(0,W.jsx)(me,{size:15}):null]},e.ID)}),a.length===0&&!s?(0,W.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function Nn({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim().replace(/^@/,``));try{o((await k.bots(e)).rows??[])}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{d()},[]),(0,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,W.jsx)(ut,{size:13}),` `,`Clear`]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(me,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:t.FirstName||`-`}),(0,W.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,W.jsx)(`span`,{children:H(t.Username)||`-`})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Bot username or id`}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,W.jsx)(L,{size:14,className:`spin`}):`Search`})]}),l&&(0,W.jsx)(`div`,{className:`picker-error`,children:l}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>(0,W.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:e.FirstName||`-`}),(0,W.jsx)(`span`,{children:H(e.Username)||`-`}),e.System?(0,W.jsx)(q,{tone:`warn`,children:`System`}):(0,W.jsx)(q,{children:`Regular`})]},e.ID)),a.length===0&&!s?(0,W.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function Pn({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);let e=new URLSearchParams({limit:`20`});r.trim()&&e.set(`q`,r.trim());try{o((await k.channels(e)).rows)}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{d()},[]),(0,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,W.jsx)(ut,{size:13}),` `,`Clear`]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(me,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:t.Title||`-`}),(0,W.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,W.jsx)(`span`,{children:H(t.Username)||pt(t)})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),d())},placeholder:`Search channel_id / username / title`}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:d,disabled:s,children:s?(0,W.jsx)(L,{size:14,className:`spin`}):`Search`})]}),l&&(0,W.jsx)(`div`,{className:`picker-error`,children:l}),(0,W.jsxs)(`div`,{className:`picker-results`,children:[a.map(e=>(0,W.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,W.jsx)(`span`,{className:`mono`,children:e.ID}),(0,W.jsx)(`strong`,{children:e.Title||`-`}),(0,W.jsx)(`span`,{children:H(e.Username)||pt(e)}),e.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:pt(e)})]},e.ID)),a.length===0&&!s?(0,W.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}function Fn({onClose:e,onMinted:t}){let[n,r]=(0,g.useState)(`vault`),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(`XTR`),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!1),[_,v]=(0,g.useState)(`TON`),[y,b]=(0,g.useState)(``),[x,S]=(0,g.useState)(!1),[C,w]=(0,g.useState)(``),[T,E]=(0,g.useState)(``),[D,O]=(0,g.useState)(``),k=Ct(f,u),A=m?Ct(y,_):`0`,j=k===null,M=m&&A===null,N=c.trim()!==``&&f.trim()!==``&&!j&&!M&&(n===`vault`||(n===`user`?i!==null:o!==null));function P(){let e={username:c.trim().replace(/^@/,``),currency:u,amount:k??`0`};if(n===`user`&&i&&(e.owner_user_id=String(i.ID)),n===`channel`&&o&&(e.owner_channel_id=String(o.ID)),m&&(e.crypto_currency=_,e.crypto_amount=A??`0`),C.trim()&&(e.url=C.trim()),T){let t=Date.parse(`${T}T${D||`00:00`}:00Z`);Number.isFinite(t)&&(e.purchase_date=Math.floor(t/1e3))}return e}return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Mint a collectible username`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`NFT usernames`}),(0,W.jsx)(`h2`,{children:`Mint a collectible username`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsxs)(`div`,{className:`mint-field-group`,children:[(0,W.jsx)(`div`,{className:`mint-field-group-label`,children:`1. Username`}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Username`}),(0,W.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:`durov`})]})]}),(0,W.jsxs)(`div`,{className:`mint-field-group`,children:[(0,W.jsx)(`div`,{className:`mint-field-group-label`,children:`2. Owner`}),(0,W.jsxs)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Owner type`,children:[(0,W.jsxs)(`button`,{type:`button`,className:`btn ${n===`vault`?`primary`:``}`,onClick:()=>r(`vault`),children:[(0,W.jsx)(lt,{size:15}),` `,`Vault (no owner)`]}),(0,W.jsx)(`button`,{type:`button`,className:`btn ${n===`user`?`primary`:``}`,onClick:()=>r(`user`),children:`User owner`}),(0,W.jsx)(`button`,{type:`button`,className:`btn ${n===`channel`?`primary`:``}`,onClick:()=>r(`channel`),children:`Channel owner`})]}),n===`user`&&(0,W.jsx)(jn,{label:`User owner`,value:i,onChange:a}),n===`channel`&&(0,W.jsx)(Pn,{label:`Channel owner`,value:o,onChange:s}),n===`vault`&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Mints the asset unassigned; issue it to someone later from the asset page.`})]}),(0,W.jsxs)(`div`,{className:`mint-field-group`,children:[(0,W.jsx)(`div`,{className:`mint-field-group-label`,children:`3. Price`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`A record of what it was sold for -- minting doesn't charge anyone.`}),(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Currency`}),(0,W.jsxs)(`select`,{value:u,onChange:e=>d(e.target.value),children:[(0,W.jsx)(`option`,{value:`XTR`,children:`XTR`}),(0,W.jsx)(`option`,{value:`TON`,children:`TON`}),(0,W.jsx)(`option`,{value:`USD`,children:`USD`})]})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Amount (${u})`}),(0,W.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),inputMode:`decimal`,placeholder:`1000`})]})]}),f.trim()!==``&&!j&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Clients will show: ${St(k??`0`,u)}.`}),j&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not a valid ${u} amount: digits only, at most ${String(yt(u))} decimal places.`}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` Also record a TON price`]}),m&&(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Crypto currency`}),(0,W.jsx)(`select`,{value:_,onChange:e=>v(e.target.value),children:(0,W.jsx)(`option`,{value:`TON`,children:`TON`})})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Crypto amount (${_})`}),(0,W.jsx)(`input`,{value:y,onChange:e=>b(e.target.value),inputMode:`decimal`,placeholder:`12.5`})]})]}),M&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not a valid ${_} amount: digits only, at most ${String(yt(_))} decimal places.`})]}),(0,W.jsxs)(`div`,{className:`mint-field-group`,children:[(0,W.jsx)(`button`,{type:`button`,className:`link-button`,onClick:()=>S(e=>!e),children:x?`Hide marketplace record`:`+ Add marketplace record (optional)`}),x&&(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Marketplace URL`}),(0,W.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:`https://fragment.com/username/durov`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Purchase date (UTC)`}),(0,W.jsx)(`input`,{value:T,onChange:e=>E(e.target.value),type:`date`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Purchase time (UTC)`}),(0,W.jsx)(`input`,{value:D,onChange:e=>O(e.target.value),type:`time`,step:60,disabled:!T})]})]})]})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`}),(0,W.jsx)(Z,{disabled:!N,label:`Mint username`,icon:(0,W.jsx)(Ue,{size:15}),tone:`neutral`,path:`/api/actions/mint-collectible-username`,payload:P,onDone:t})]})]})}),document.body)}function In({navigate:e}){let[t,n]=(0,g.useState)(`all`),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(`50`),[s,c]=(0,g.useState)([]),[l,u]=(0,g.useState)(!1),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(!1);async function b(e=!1){m(!0),_(``);let n=new URLSearchParams({limit:a});t!==`all`&&n.set(`status`,t),r.trim()&&n.set(`q`,r.trim().replace(/^@/,``)),e&&d&&n.set(`before_id`,d);try{let t=await k.collectibleUsernames(n),r=t.rows??[];c(t=>e?[...t,...r]:r),f(t.next_before_id??``),u(!!t.has_more)}catch(e){_(O(e))}finally{m(!1)}}(0,g.useEffect)(()=>{b(!1)},[]);let x=s.filter(e=>e.Status===`vault`).length,S=s.filter(e=>e.Status===`owned`).length,C=s.filter(e=>e.Status===`burned`).length;return(0,W.jsxs)(Dt,{title:`Collectible usernames`,eyebrow:`NFT usernames / Registry`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>y(!0),children:[(0,W.jsx)(Ue,{size:15}),` `,`Mint username`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>b(!1),disabled:p,children:[(0,W.jsx)(Ke,{size:15,className:p?`spin`:``}),` `,`Refresh`]})]}),children:[h&&(0,W.jsx)(K,{children:h}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Loaded rows`,value:String(s.length)}),(0,W.jsx)(J,{label:`In vault`,value:String(x)}),(0,W.jsx)(J,{label:`Held by owners`,value:String(S),tone:`good`}),(0,W.jsx)(J,{label:`Burned`,value:String(C),tone:C?`danger`:`neutral`})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),b(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`Search by username`})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Status`}),(0,W.jsxs)(`select`,{value:t,onChange:e=>n(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All statuses`}),(0,W.jsx)(`option`,{value:`vault`,children:`Vault`}),(0,W.jsx)(`option`,{value:`owned`,children:`Owned`}),(0,W.jsx)(`option`,{value:`burned`,children:`Burned`})]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsx)(`input`,{className:`small-input`,value:a,onChange:e=>o(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:p,children:[p?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,`Search`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Username`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Owner`}),(0,W.jsx)(`th`,{children:`Price`}),(0,W.jsx)(`th`,{children:`Purchase date (UTC)`}),(0,W.jsx)(`th`,{children:`Transfers`}),(0,W.jsx)(`th`,{children:`Updated`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[s.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:(0,W.jsx)(`strong`,{children:H(t.Username)})}),(0,W.jsx)(`td`,{children:(0,W.jsx)(Ln,{status:t.Status})}),(0,W.jsx)(`td`,{children:Rn(t,`Vault`)}),(0,W.jsx)(`td`,{className:`mono`,children:zn(t)}),(0,W.jsx)(`td`,{children:U(t.PurchaseDate)||`-`}),(0,W.jsx)(`td`,{className:`mono`,children:t.TransferCount}),(0,W.jsx)(`td`,{children:U(t.UpdatedAt)||`-`}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/collectible-usernames/${t.ID}`),children:[(0,W.jsx)(ue,{size:14}),` `,`Details`,` `,(0,W.jsx)(_e,{size:14})]})})]},t.ID)),s.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})}),l&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>b(!0),disabled:p,children:[p?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(he,{size:15}),` `,`Load more`]})}),v&&(0,W.jsx)(Fn,{onClose:()=>y(!1),onMinted:()=>void b(!1)})]})}function Ln({status:e}){return e===`owned`?(0,W.jsx)(q,{tone:`good`,children:`Owned`}):e===`burned`?(0,W.jsxs)(q,{tone:`danger`,children:[(0,W.jsx)(Te,{size:12}),` `,`Burned`]}):(0,W.jsxs)(q,{children:[(0,W.jsx)(lt,{size:12}),` `,`Vault`]})}function Rn(e,t){return!e.OwnerPeerType||e.OwnerPeerID===``||e.OwnerPeerID===`0`?t:`${H(e.OwnerUsername)||e.OwnerName||e.OwnerPeerID} · ${e.OwnerPeerType}:${e.OwnerPeerID}`}function zn(e){let t=St(e.Amount,e.Currency);return e.CryptoCurrency&&e.CryptoAmount&&e.CryptoAmount!==`0`?`${t} (${St(e.CryptoAmount,e.CryptoCurrency)})`:t}function Bn({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(`user`),[f,p]=(0,g.useState)(null),[m,h]=(0,g.useState)(null);async function _(){s(!0),a(``);try{r(await k.collectibleUsername(e))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{_(),l(`profile`)},[e]),i&&!n)return(0,W.jsx)(K,{children:i});if(!n)return(0,W.jsx)(X,{label:o?`Loading collectible username…`:`Waiting for data`});let v=n.asset,y=n.transfers??[],b=`Vault`,x=!!v.OwnerPeerType&&v.OwnerPeerID!==``&&v.OwnerPeerID!==`0`,S=v.Status===`burned`;function C(){x&&t(v.OwnerPeerType===`channel`?`/channels/${v.OwnerPeerID}`:`/accounts/${v.OwnerPeerID}`)}function w(){let e={username:v.Username};return u===`user`&&f&&(e.to_user_id=String(f.ID)),u===`channel`&&m&&(e.to_channel_id=String(m.ID)),e}let T=[{key:`profile`,label:`Profile & Status`,icon:(0,W.jsx)(ae,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,W.jsx)(Ye,{size:15})}];return(0,W.jsxs)(Dt,{title:`Collectible ${H(v.Username)}`,eyebrow:`NFT usernames / Asset`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/collectible-usernames`),children:[(0,W.jsx)(le,{size:15}),` `,`Back to list`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:_,disabled:o,children:[(0,W.jsx)(Ke,{size:15,className:o?`spin`:``}),` `,`Refresh`]})]}),children:[i&&(0,W.jsx)(K,{children:i}),(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsx)(`div`,{className:`entity-head-main`,children:(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:H(v.Username)}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`Asset #${v.ID}`})]})}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(Ln,{status:v.Status}),(0,W.jsx)(q,{tone:v.TransferCount>0?`warn`:`neutral`,children:`${v.TransferCount} transfers`}),v.Status===`owned`&&(0,W.jsx)(q,{tone:v.RegistryActive?`good`:`warn`,children:v.RegistryActive?`Active in profile`:`Hidden in profile`})]})]}),(0,W.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Asset sections`,children:T.map(e=>(0,W.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Owner`,value:Rn(v,b)}),(0,W.jsx)(Y,{label:`Price`,value:zn(v),mono:!0}),(0,W.jsx)(Y,{label:`Purchase date (UTC)`,value:U(v.PurchaseDate)||`-`}),(0,W.jsx)(Y,{label:`Original owner`,value:Un(v.OriginalOwnerPeerType,v.OriginalOwnerPeerID,b,v.OriginalOwnerUsername)}),(0,W.jsx)(Y,{label:`Transfers`,value:String(v.TransferCount),mono:!0}),(0,W.jsx)(Y,{label:`Created`,value:U(v.CreatedAt)||`-`}),(0,W.jsx)(Y,{label:`Updated`,value:U(v.UpdatedAt)||`-`})]}),(0,W.jsxs)(`div`,{className:`toolbar`,children:[x&&(0,W.jsx)(`button`,{className:`row-link`,type:`button`,onClick:C,children:v.OwnerPeerType===`channel`?`Open owner channel`:`Open owner account`}),v.URL&&(0,W.jsxs)(`a`,{className:`row-link`,href:v.URL,target:`_blank`,rel:`noreferrer noopener`,children:[(0,W.jsx)(xe,{size:14}),` `,`Open marketplace page`]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Provenance history`,text:`Mint, transfer, revoke and burn events in chronological order.`,action:(0,W.jsx)(qe,{size:16})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Event`}),(0,W.jsx)(`th`,{children:`From`}),(0,W.jsx)(`th`,{children:`To`}),(0,W.jsx)(`th`,{children:`Price`}),(0,W.jsx)(`th`,{children:`Actor`}),(0,W.jsx)(`th`,{children:`Reason`}),(0,W.jsx)(`th`,{children:`Time`})]})}),(0,W.jsxs)(`tbody`,{children:[y.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:e.ID}),(0,W.jsx)(`td`,{children:(0,W.jsx)(Hn,{kind:e.Kind})}),(0,W.jsx)(`td`,{className:`mono`,children:Un(e.FromPeerType,e.FromPeerID,b,e.FromUsername)}),(0,W.jsx)(`td`,{className:`mono`,children:Un(e.ToPeerType,e.ToPeerID,b,e.ToUsername)}),(0,W.jsx)(`td`,{className:`mono`,children:e.Amount&&e.Amount!==`0`?St(e.Amount,e.Currency):`-`}),(0,W.jsx)(`td`,{children:e.Actor||`-`}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Reason||`-`}),(0,W.jsx)(`td`,{children:U(e.CreatedAt)||`-`})]},e.ID)),y.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})})]})]}),c===`actions`&&(0,W.jsx)(`div`,{className:`stacked-sections`,children:S?(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Asset Operations`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This username is burned — no further operations are possible.`})})]}):(0,W.jsxs)(`div`,{className:`action-groups`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Transfer Ownership`,text:`Sent immediately; appended to the provenance history.`}),(0,W.jsxs)(`div`,{className:`card-body`,children:[(0,W.jsxs)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Recipient type`,children:[(0,W.jsx)(`button`,{type:`button`,className:`btn ${u===`user`?`primary`:``}`,onClick:()=>d(`user`),children:`To user`}),(0,W.jsx)(`button`,{type:`button`,className:`btn ${u===`channel`?`primary`:``}`,onClick:()=>d(`channel`),children:`To channel`})]}),u===`user`?(0,W.jsx)(jn,{label:`To user`,value:f,onChange:p}):(0,W.jsx)(Pn,{label:`To channel`,value:m,onChange:h}),(0,W.jsx)(Z,{label:`Transfer`,icon:(0,W.jsx)(ce,{size:15}),tone:`warn`,path:`/api/actions/transfer-collectible-username`,payload:w,onDone:_})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Revoke To Vault`,text:`Returns the username to the vault; it can be issued again later.`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(`div`,{className:`action-stack`,children:(0,W.jsx)(Z,{label:`Revoke to vault`,icon:(0,W.jsx)(at,{size:15}),tone:`warn`,path:`/api/actions/revoke-collectible-username`,payload:()=>({username:v.Username,burn:!1}),onDone:_})})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Danger Zone`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`danger-zone`,children:[(0,W.jsx)(Z,{label:`Burn permanently`,icon:(0,W.jsx)(Te,{size:15}),tone:`danger`,path:`/api/actions/revoke-collectible-username`,payload:()=>({username:v.Username,burn:!0}),onDone:_}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Irreversible: the username is destroyed and can never be issued again.`}),(0,W.jsx)(Z,{label:`Delete record`,icon:(0,W.jsx)(it,{size:15}),tone:`danger`,path:`/api/actions/delete-collectible-username`,payload:()=>({username:v.Username}),onDone:()=>t(`/collectible-usernames`)}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Erases the asset and its ownership history, and frees the username for a fresh issue. Use this for a username issued by mistake; a burn keeps the history instead.`})]})})]})]})})]})}var Vn={mint:`Mint`,transfer:`Transfer`,burn:`Burn`,revoke:`Revoke`};function Hn({kind:e}){return(0,W.jsx)(q,{tone:e===`burn`?`danger`:e===`revoke`?`warn`:e===`mint`?`good`:`neutral`,children:Vn[e]})}function Un(e,t,n,r=``){if(!e||t===``||t===`0`)return n;let i=H(r);return i?`${i} · ${e}:${t}`:`${e}:${t}`}function Wn(){let[e,t]=(0,g.useState)(``),[n,r]=(0,g.useState)(!1),[i,a]=(0,g.useState)([]),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(``);async function u(){s(!0),l(``);let t=new URLSearchParams({limit:`200`});e.trim()&&t.set(`q`,e.trim().replace(/^@/,``));try{a((await k.reservedUsernames(t)).reserved??[])}catch(e){l(O(e))}finally{s(!1)}}return(0,g.useEffect)(()=>{u()},[]),(0,W.jsxs)(Dt,{title:`Reserved usernames`,eyebrow:`Usernames / Blocklist`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>r(!0),children:[(0,W.jsx)(Ue,{size:15}),` `,`Reserve username`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>u(),disabled:o,children:[(0,W.jsx)(Ke,{size:15,className:o?`spin`:``}),` `,`Refresh`]})]}),children:[c&&(0,W.jsx)(K,{children:c}),(0,W.jsx)(`div`,{className:`metric-row`,children:(0,W.jsx)(J,{label:`Reserved names`,value:String(i.length)})}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),u()},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:e,onChange:e=>t(e.target.value),placeholder:`Filter by prefix`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:o,children:[o?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,`Search`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Username`}),(0,W.jsx)(`th`,{children:`Reason`}),(0,W.jsx)(`th`,{children:`Reserved by`}),(0,W.jsx)(`th`,{children:`Reserved (UTC)`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[i.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:(0,W.jsx)(`strong`,{children:`@${e.username}`})}),(0,W.jsx)(`td`,{children:e.reason||`-`}),(0,W.jsx)(`td`,{children:e.actor||`-`}),(0,W.jsx)(`td`,{children:mt(e.created_at)||`-`}),(0,W.jsx)(`td`,{children:(0,W.jsx)(Z,{compact:!0,label:`Unreserve`,icon:(0,W.jsx)(it,{size:13}),tone:`danger`,path:`/api/actions/unreserve-username`,payload:()=>({username:e.username}),onDone:()=>void u()})})]},e.username)),i.length===0&&(0,W.jsx)(jt,{colSpan:5})]})]})}),n&&(0,W.jsx)(Gn,{onClose:()=>r(!1),onDone:()=>{r(!1),u()}})]})}function Gn({onClose:e,onDone:t}){let[n,r]=(0,g.useState)(``),i=n.trim().replace(/^@/,``);return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Reserve a username`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`Usernames`}),(0,W.jsx)(`h2`,{children:`Reserve a username`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsxs)(`label`,{className:`form-field`,children:[(0,W.jsx)(`span`,{children:`Username`}),(0,W.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:`support`,autoFocus:!0})]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`No peer will be able to take @${i||`…`} until it is unreserved. Nothing is shown to users.`})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`}),(0,W.jsx)(Z,{disabled:i.length<5,label:`Reserve username`,icon:(0,W.jsx)(Ue,{size:15}),tone:`neutral`,path:`/api/actions/reserve-username`,payload:()=>({username:i}),onDone:t})]})]})}),document.body)}function Kn({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(0);async function m(){s(!0),a(``);try{r(await k.channel(e))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{m(),l(`profile`)},[e]),i)return(0,W.jsx)(K,{children:i});if(!n)return(0,W.jsx)(X,{label:o?`Loading channel detail`:`Waiting for data`});let h=n.Channel,_=[{key:`profile`,label:`Profile & Status`,icon:(0,W.jsx)(ae,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,W.jsx)(Ye,{size:15})}];return(0,W.jsxs)(Dt,{title:`${pt(h)} #${h.ID}`,eyebrow:`Channel Profile`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,W.jsx)(le,{size:15}),` `,`Back to list`]}),children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{className:`entity-head-main`,children:[(0,W.jsxs)(`div`,{className:`avatar-edit-slot`,children:[(0,W.jsx)(fn,{id:h.ID,kind:`channel`,title:h.Title,size:64,refreshKey:f||void 0}),(0,W.jsx)(`button`,{className:`icon-btn avatar-edit-btn`,type:`button`,"aria-label":`Change avatar`,title:`Change avatar`,onClick:()=>d(!0),children:(0,W.jsx)(Ae,{size:13})})]}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:h.Title||`-`}),(0,W.jsxs)(`div`,{className:`entity-subtitle`,children:[H(h.Username)||`No username`,` · `,`Creator ${h.CreatorUserID}`]})]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(q,{children:pt(h)}),h.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Not verified`}),(0,W.jsx)(mn,{scam:h.Scam,fake:h.Fake}),h.Deleted?(0,W.jsx)(q,{tone:`danger`,children:`Deleted`}):(0,W.jsx)(q,{children:`Valid`})]})]}),(0,W.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Channel sections`,children:_.map(e=>(0,W.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Channel ID`,value:String(h.ID),mono:!0}),(0,W.jsx)(Y,{label:`access_hash`,value:String(h.AccessHash),mono:!0}),(0,W.jsx)(Y,{label:`Members`,value:`${h.ParticipantsCount} / Admins ${h.AdminsCount}`}),(0,W.jsx)(Y,{label:`Moderation`,value:`Banned ${h.BannedCount} / Kicked ${h.KickedCount}`}),(0,W.jsx)(Y,{label:`Channel flags`,value:`broadcast=${h.Broadcast} megagroup=${h.Megagroup} forum=${h.Forum}`}),(0,W.jsx)(Y,{label:`top / pinned / PTS`,value:`${h.TopMessageID} / ${h.PinnedMessageID} / ${h.PTS}`}),(0,W.jsx)(Y,{label:`Created`,value:mt(h.Date)||`-`}),(0,W.jsx)(Y,{label:`Updated`,value:U(h.UpdatedAt)||`-`})]}),h.About&&(0,W.jsx)(`p`,{className:`about-text`,children:h.About}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Channel Raw Row`,text:`Database read-only snapshot`}),(0,W.jsx)(Mt,{value:n.ChannelJSON})]})]}),c===`actions`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`action-groups`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Verification & Moderation Flags`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:h.Verified?`Clear verified`:`Set verified`,icon:(0,W.jsx)(F,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:h.ID,verified:!h.Verified}),onDone:m}),(0,W.jsx)(hn,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-flags`,scam:h.Scam,fake:h.Fake,onDone:m})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Settings`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(Cn,{channel:h,onDone:m})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Username`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(_n,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-username`,current:h.Username,onDone:m})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Profile Color`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(xn,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-color`,onDone:m})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Emoji Status`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(Sn,{idKey:`channel_id`,id:h.ID,path:`/api/actions/set-channel-emoji-status`,onDone:m})})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Recent Admin Actions`,text:`Last 30 audit rows`,action:(0,W.jsx)(qe,{size:16})}),(0,W.jsx)(At,{rows:n.AuditLogs})]})]}),u&&(0,W.jsx)(sn,{kind:`channel`,id:h.ID,onClose:()=>d(!1),onDone:()=>{p(e=>e+1),m()}})]})}var qn={beforeID:0,beforeUpdatedUS:0};function Jn({navigate:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(50),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)([]),[l,u]=(0,g.useState)(qn),[d,f]=(0,g.useState)(!1),[p,m]=(0,g.useState)(``);async function h(e,t){f(!0),m(``);let n=new URLSearchParams({limit:String(r)});e.trim()&&n.set(`q`,e.trim()),(t.beforeID||t.beforeUpdatedUS)&&(n.set(`before_id`,String(t.beforeID)),n.set(`before_updated_us`,String(t.beforeUpdatedUS)));try{let e=await k.channels(n);return o(e),e}catch(e){return m(O(e)),null}finally{f(!1)}}async function _(){c([]),u(qn),await h(t,qn)}async function v(){if(!a?.has_more)return;let e={beforeID:a.next_before_id,beforeUpdatedUS:a.next_before_updated_us};await h(t,e)&&(c(e=>[...e,l]),u(e))}async function y(){if(s.length===0)return;let e=s[s.length-1];await h(t,e)&&(c(e=>e.slice(0,-1)),u(e))}(0,g.useEffect)(()=>{_()},[]);let b=Dn(a?.rows??[]),x=s.length>0&&!d,S=!!a?.has_more&&!d;return(0,W.jsxs)(Dt,{title:`Supergroups and Channels`,eyebrow:a?.listing===!1?`Search results`:`Recently updated`,actions:(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>void _(),disabled:d,children:[(0,W.jsx)(Ke,{size:15}),` `,`Refresh`]}),children:[p&&(0,W.jsx)(K,{children:p}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Entities on page`,value:String(a?.rows.length??0)}),(0,W.jsx)(J,{label:`Supergroups`,value:String(b.megagroups)}),(0,W.jsx)(J,{label:`Channels`,value:String(b.broadcasts)}),(0,W.jsx)(J,{label:`Verified`,value:String(b.verified),tone:`good`})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),_()},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:`Channel ID / username / title`})]}),(0,W.jsxs)(`label`,{className:`gift-page-size`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsxs)(`select`,{value:String(r),onChange:e=>i(Number(e.target.value)),children:[(0,W.jsx)(`option`,{value:`10`,children:`10`}),(0,W.jsx)(`option`,{value:`20`,children:`20`}),(0,W.jsx)(`option`,{value:`50`,children:`50`}),(0,W.jsx)(`option`,{value:`100`,children:`100`})]})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:d,children:[d?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,`Search`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void y(),disabled:!x,children:[(0,W.jsx)(ge,{size:15}),` `,`Previous page`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void v(),disabled:!S,children:[(0,W.jsx)(_e,{size:15}),` `,`Next page`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{className:`avatar-col`}),(0,W.jsx)(`th`,{children:`Channel ID`}),(0,W.jsx)(`th`,{children:`Kind`}),(0,W.jsx)(`th`,{children:`Username`}),(0,W.jsx)(`th`,{children:`Title`}),(0,W.jsx)(`th`,{children:`Members`}),(0,W.jsx)(`th`,{children:`Admins`}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Verified`}),(0,W.jsx)(`th`,{children:`Updated`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[a?.rows.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`avatar-col`,children:(0,W.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/channels/${t.ID}`),"aria-label":`Open channel ${t.ID}`,children:(0,W.jsx)(fn,{id:t.ID,kind:`channel`,title:t.Title})})}),(0,W.jsx)(`td`,{className:`mono`,children:t.ID}),(0,W.jsx)(`td`,{children:pt(t)}),(0,W.jsx)(`td`,{children:H(t.Username)}),(0,W.jsx)(`td`,{children:t.Title}),(0,W.jsx)(`td`,{children:t.ParticipantsCount}),(0,W.jsx)(`td`,{children:t.AdminsCount}),(0,W.jsx)(`td`,{children:t.PTS}),(0,W.jsxs)(`td`,{children:[t.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Not verified`}),` `,(0,W.jsx)(mn,{scam:t.Scam,fake:t.Fake})]}),(0,W.jsx)(`td`,{children:U(t.UpdatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${t.ID}`),children:[`Details`,` `,(0,W.jsx)(_e,{size:14})]})})]},t.ID)),(!a||a.rows.length===0)&&(0,W.jsx)(jt,{colSpan:11})]})]})})]})}function Yn({botID:e,onClose:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(!1),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(!1);async function u(){if(!n.trim()){s(`Please enter an operation reason`);return}a(!0),s(``),l(!1);try{let t=await k.action(`/api/actions/export-bot-token`,{command_id:``,reason:n.trim(),confirm:!0,bot_user_id:e}),r=t.details?.token;if(t.error||typeof r!=`string`||!r){s(t.error||`No token returned.`);return}await navigator.clipboard.writeText(r),l(!0)}catch(e){s(O(e))}finally{a(!1)}}return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Copy bot token`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`Bot`}),(0,W.jsx)(`h2`,{children:`Copy bot token`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:i,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsx)(`p`,{children:`The token is written straight to your clipboard and is never shown on screen. Paste it wherever it's needed right after copying.`}),(0,W.jsxs)(`label`,{className:`form-field`,children:[(0,W.jsx)(`span`,{children:`Operation reason`}),(0,W.jsx)(`textarea`,{value:n,onChange:e=>r(e.target.value),rows:3,placeholder:`Describe why this token is being retrieved`})]}),o&&(0,W.jsx)(K,{children:o}),c&&(0,W.jsx)(`div`,{className:`secret-reveal`,children:(0,W.jsxs)(`div`,{className:`secret-reveal-label`,children:[(0,W.jsx)(me,{size:14}),` `,`Token copied to clipboard.`]})})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:i,children:`Close`}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>void u(),disabled:i,children:[(0,W.jsx)(ve,{size:15}),` `,c?`Copy again`:`Copy token`]})]})]})}),document.body)}function Xn({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(`profile`),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(0),[m,h]=(0,g.useState)(!1);async function _(){s(!0),a(``);try{r(await k.bot(e))}catch(e){a(O(e))}finally{s(!1)}}if((0,g.useEffect)(()=>{_(),l(`profile`)},[e]),i)return(0,W.jsx)(K,{children:i});if(!n)return(0,W.jsx)(X,{label:o?`Loading bot detail`:`Waiting for data`});let v=n.Bot,y=[{key:`profile`,label:`Profile & Status`,icon:(0,W.jsx)(ae,{size:15})},{key:`actions`,label:`Actions & Management`,icon:(0,W.jsx)(Ye,{size:15})}];return(0,W.jsxs)(Dt,{title:`Bot #${v.ID}`,eyebrow:`Bot Profile`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/bots`),children:[(0,W.jsx)(le,{size:15}),` `,`Back to list`]}),children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{className:`entity-head-main`,children:[(0,W.jsxs)(`div`,{className:`avatar-edit-slot`,children:[(0,W.jsx)(fn,{id:v.ID,firstName:v.FirstName,username:v.Username,size:64,refreshKey:f||void 0}),(0,W.jsx)(`button`,{className:`icon-btn avatar-edit-btn`,type:`button`,"aria-label":`Change avatar`,title:`Change avatar`,onClick:()=>d(!0),children:(0,W.jsx)(Ae,{size:13})})]}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:v.FirstName||`Unnamed bot`}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:H(v.Username)||`No username`})]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(q,{tone:v.System?`warn`:`neutral`,children:v.System?`System`:`User`}),v.Verified?(0,W.jsx)(q,{tone:`good`,children:`Verified`}):(0,W.jsx)(q,{children:`Not verified`}),(0,W.jsx)(mn,{scam:v.Scam,fake:v.Fake})]})]}),(0,W.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Bot sections`,children:y.map(e=>(0,W.jsxs)(`button`,{className:`btn icon-text ${c===e.key?`primary`:``}`,type:`button`,"aria-pressed":c===e.key,onClick:()=>l(e.key),children:[e.icon,` `,e.label]},e.key))}),c===`profile`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Bot ID`,value:String(v.ID),mono:!0}),(0,W.jsx)(Y,{label:`Owner`,value:v.OwnerUserID>0?`${v.OwnerUserID} ${H(n.OwnerUsername)}`.trim():`None`}),(0,W.jsx)(Y,{label:`Type`,value:v.System?`System`:`User`}),(0,W.jsx)(Y,{label:`Updated`,value:U(v.UpdatedAt)||`-`}),(0,W.jsx)(Y,{label:`Created`,value:U(v.CreatedAt)||`-`})]}),n.About&&(0,W.jsx)(`p`,{className:`about-text`,children:n.About}),n.Description&&n.Description.trim()!==n.About.trim()&&(0,W.jsx)(`p`,{className:`about-text`,children:n.Description})]}),c===`actions`&&(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`action-groups`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Verification & Moderation Flags`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:v.Verified?`Clear verified`:`Set verified`,icon:(0,W.jsx)(F,{size:15}),tone:`neutral`,path:`/api/actions/set-verified`,payload:()=>({user_id:v.ID,verified:!v.Verified}),onDone:_}),(0,W.jsx)(hn,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-flags`,scam:v.Scam,fake:v.Fake,onDone:_})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Username`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(_n,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-username`,current:v.Username,onDone:_})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Profile Color`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(xn,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-color`,onDone:_})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Emoji Status`}),(0,W.jsx)(`div`,{className:`card-body`,children:(0,W.jsx)(Sn,{idKey:`user_id`,id:v.ID,path:`/api/actions/set-account-emoji-status`,onDone:_})})]}),!v.System&&(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Credentials`}),(0,W.jsxs)(`div`,{className:`card-body`,children:[(0,W.jsx)(`div`,{className:`action-stack`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>h(!0),children:[(0,W.jsx)(ve,{size:15}),` `,`Copy token`]})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Copies straight to the clipboard through a dedicated confirmation step -- the token itself is never shown on this page.`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Danger Zone`}),(0,W.jsx)(`div`,{className:`card-body`,children:v.System?(0,W.jsx)(`p`,{className:`bot-create-note`,children:`System bots are built in and cannot be deleted.`}):(0,W.jsxs)(`div`,{className:`danger-zone`,children:[(0,W.jsx)(Z,{label:`Delete bot`,icon:(0,W.jsx)(it,{size:15}),tone:`danger`,path:`/api/actions/delete-bot`,payload:()=>({bot_user_id:v.ID}),onDone:()=>t(`/bots`)}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Permanently deletes this user-created bot and invalidates its token. This cannot be undone.`})]})})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Recent Admin Actions`,text:`Last 30 audit rows`,action:(0,W.jsx)(qe,{size:16})}),(0,W.jsx)(At,{rows:n.AuditLogs})]})]}),u&&(0,W.jsx)(sn,{kind:`user`,id:v.ID,onClose:()=>d(!1),onDone:()=>{p(e=>e+1),_()}}),m&&(0,W.jsx)(Yn,{botID:v.ID,onClose:()=>h(!1)})]})}function Zn({onClose:e,onCreated:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``);return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Create bot`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`Bots`}),(0,W.jsx)(`h2`,{children:`Create bot`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsx)(`p`,{children:`Provision a bot account owned by the given user. The token is shown once after confirmation.`}),(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Owner user ID`}),(0,W.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),type:`number`,min:`1`,placeholder:`123456789`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Display name`}),(0,W.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:`e.g. Service Bot`,maxLength:64})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Username`}),(0,W.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:`my_service_bot`})]})]}),(0,W.jsx)(`span`,{className:`bot-create-note`,children:`Username must be 5-32 characters and end with 'bot'.`})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`}),(0,W.jsx)(Z,{label:`Create bot`,icon:(0,W.jsx)(Ue,{size:15}),tone:`neutral`,path:`/api/actions/create-bot`,payload:()=>({owner_user_id:gt(n),name:i.trim(),username:o.trim().replace(/^@/,``)}),secretField:`token`,onDone:t})]})]})}),document.body)}var Qn={beforeID:0};function $n({navigate:e}){let[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(50),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)([]),[l,u]=(0,g.useState)(Qn),[d,f]=(0,g.useState)(!1),[p,m]=(0,g.useState)(``),[h,_]=(0,g.useState)(!1);async function v(e,t){f(!0),m(``);let n=new URLSearchParams({limit:String(r)});e.trim()&&n.set(`q`,e.trim()),t.beforeID&&n.set(`before_id`,String(t.beforeID));try{let e=await k.bots(n);return o(e),e}catch(e){return m(O(e)),null}finally{f(!1)}}async function y(){c([]),u(Qn),await v(t,Qn)}async function b(){if(!a?.has_more)return;let e={beforeID:a.next_before_id};await v(t,e)&&(c(e=>[...e,l]),u(e))}async function x(){if(s.length===0)return;let e=s[s.length-1];await v(t,e)&&(c(e=>e.slice(0,-1)),u(e))}(0,g.useEffect)(()=>{y()},[]);let S=a?.rows??[],C=S.filter(e=>e.Verified).length,w=S.filter(e=>e.System).length,T=s.length>0&&!d,E=!!a?.has_more&&!d;return(0,W.jsxs)(Dt,{title:`Bots`,eyebrow:a?.listing===!1?`Search results`:`Recently created bots`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>_(!0),children:[(0,W.jsx)(Ue,{size:15}),` `,`Create bot`]}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>void y(),disabled:d,children:[(0,W.jsx)(Ke,{size:15}),` `,`Refresh`]})]}),children:[p&&(0,W.jsx)(K,{children:p}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Bots on page`,value:String(S.length)}),(0,W.jsx)(J,{label:`Verified`,value:String(C),tone:`good`}),(0,W.jsx)(J,{label:`System`,value:String(w)})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),y()},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:`Bot ID / username`})]}),(0,W.jsxs)(`label`,{className:`gift-page-size`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsxs)(`select`,{value:String(r),onChange:e=>i(Number(e.target.value)),children:[(0,W.jsx)(`option`,{value:`10`,children:`10`}),(0,W.jsx)(`option`,{value:`20`,children:`20`}),(0,W.jsx)(`option`,{value:`50`,children:`50`}),(0,W.jsx)(`option`,{value:`100`,children:`100`})]})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:d,children:[d?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,`Search`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void x(),disabled:!T,children:[(0,W.jsx)(ge,{size:15}),` `,`Previous page`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void b(),disabled:!E,children:[(0,W.jsx)(_e,{size:15}),` `,`Next page`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{className:`avatar-col`}),(0,W.jsx)(`th`,{children:`Bot ID`}),(0,W.jsx)(`th`,{children:`Username`}),(0,W.jsx)(`th`,{children:`Name`}),(0,W.jsx)(`th`,{children:`Owner`}),(0,W.jsx)(`th`,{children:`Verified`}),(0,W.jsx)(`th`,{children:`Type`}),(0,W.jsx)(`th`,{children:`Created`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[S.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`avatar-col`,children:(0,W.jsx)(`button`,{className:`avatar-link`,type:`button`,onClick:()=>e(`/bots/${t.ID}`),"aria-label":`Open bot ${t.ID}`,children:(0,W.jsx)(fn,{id:t.ID,firstName:t.FirstName,username:t.Username})})}),(0,W.jsx)(`td`,{className:`mono`,children:t.ID}),(0,W.jsx)(`td`,{children:H(t.Username)||`-`}),(0,W.jsx)(`td`,{children:t.FirstName||`-`}),(0,W.jsx)(`td`,{className:`mono`,children:t.OwnerUserID>0?t.OwnerUserID:`-`}),(0,W.jsxs)(`td`,{children:[t.Verified?(0,W.jsxs)(q,{tone:`good`,children:[(0,W.jsx)(F,{size:12}),` `,`Verified`]}):(0,W.jsx)(q,{children:`Not verified`}),` `,(0,W.jsx)(mn,{scam:t.Scam,fake:t.Fake})]}),(0,W.jsx)(`td`,{children:t.System?(0,W.jsx)(q,{tone:`warn`,children:`System`}):(0,W.jsx)(q,{children:`User`})}),(0,W.jsx)(`td`,{children:U(t.CreatedAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/bots/${t.ID}`),children:[(0,W.jsx)(R,{size:14}),` `,`Details`,` `,(0,W.jsx)(_e,{size:14})]})})]},t.ID)),S.length===0&&(0,W.jsx)(jt,{colSpan:9})]})]})}),h&&(0,W.jsx)(Zn,{onClose:()=>_(!1),onCreated:()=>void y()})]})}function er({onClose:e,onCreated:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`all`),[o,s]=(0,g.useState)([]),c=(0,g.useMemo)(()=>!n.trim()||i===`selected`&&o.length===0,[n,i,o]);return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Send broadcast`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`Broadcasts`}),(0,W.jsx)(`h2`,{children:`Send broadcast`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsx)(`p`,{children:`Sends a message from the official system account (777000) to all users or to a chosen list. Delivery happens in the background and may take a few minutes for large audiences.`}),(0,W.jsxs)(`label`,{className:`form-field`,children:[(0,W.jsx)(`span`,{children:`Message`}),(0,W.jsx)(`textarea`,{value:n,onChange:e=>r(e.target.value),rows:5,maxLength:4096,placeholder:`What's new...`})]}),(0,W.jsx)(`div`,{className:`bot-create-fields`,children:(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Target`}),(0,W.jsxs)(`select`,{value:i,onChange:e=>a(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All users`}),(0,W.jsx)(`option`,{value:`selected`,children:`Selected users`})]})]})}),i===`selected`&&(0,W.jsx)(Mn,{label:`Recipients`,selected:o,onChange:s})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,children:`Close`}),(0,W.jsx)(Z,{label:`Send broadcast`,icon:(0,W.jsx)(Je,{size:15}),tone:`neutral`,path:`/api/actions/create-broadcast`,disabled:c,payload:()=>({message:n.trim(),target_mode:i,user_ids:i===`selected`?o.map(e=>e.ID):void 0}),onDone:t})]})]})}),document.body)}var tr={beforeID:0};function nr(){let[e,t]=(0,g.useState)(null),[n,r]=(0,g.useState)([]),[i,a]=(0,g.useState)(tr),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(!1);async function f(e){s(!0),l(``);let n=new URLSearchParams({limit:`50`});e.beforeID&&n.set(`before_id`,String(e.beforeID));try{let e=await k.broadcasts(n);return t(e),e}catch(e){return l(O(e)),null}finally{s(!1)}}async function p(){r([]),a(tr),await f(tr)}async function m(){if(!e?.has_more)return;let t={beforeID:e.next_before_id};await f(t)&&(r(e=>[...e,i]),a(t))}async function h(){if(n.length===0)return;let e=n[n.length-1];await f(e)&&(r(e=>e.slice(0,-1)),a(e))}(0,g.useEffect)(()=>{p()},[]);let _=e?.rows??[],v=_.filter(e=>e.SentCount+e.FailedCount0&&!o,b=!!e?.has_more&&!o;return(0,W.jsxs)(Dt,{title:`Broadcasts`,eyebrow:`Announcements sent from the official system account`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>d(!0),children:[(0,W.jsx)(Je,{size:15}),` `,`Send broadcast`]}),(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>void p(),disabled:o,children:[(0,W.jsx)(Ke,{size:15}),` `,`Refresh`]})]}),children:[c&&(0,W.jsx)(K,{children:c}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Campaigns on page`,value:String(_.length)}),(0,W.jsx)(J,{label:`Still delivering`,value:String(v),tone:v>0?`warn`:`neutral`})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Message`}),(0,W.jsx)(`th`,{children:`Target`}),(0,W.jsx)(`th`,{children:`Sent`}),(0,W.jsx)(`th`,{children:`Failed`}),(0,W.jsx)(`th`,{children:`Total`}),(0,W.jsx)(`th`,{children:`Created by`}),(0,W.jsx)(`th`,{children:`Created`})]})}),(0,W.jsxs)(`tbody`,{children:[_.map(e=>{let t=e.SentCount+e.FailedCount,n=e.TotalCount>0&&t>=e.TotalCount;return(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:e.ID}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Message}),(0,W.jsx)(`td`,{children:e.TargetMode===`all`?(0,W.jsx)(q,{tone:`warn`,children:`All users`}):(0,W.jsx)(q,{children:`Selected`})}),(0,W.jsx)(`td`,{children:e.SentCount}),(0,W.jsx)(`td`,{children:e.FailedCount>0?(0,W.jsx)(q,{tone:`danger`,children:e.FailedCount}):e.FailedCount}),(0,W.jsx)(`td`,{children:e.TotalCount}),(0,W.jsx)(`td`,{children:e.CreatedBy||`-`}),(0,W.jsxs)(`td`,{children:[U(e.CreatedAt),!n&&(0,W.jsx)(q,{tone:`warn`,children:`Sending`})]})]},e.ID)}),_.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})}),(0,W.jsxs)(`div`,{className:`toolbar`,children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void h(),disabled:!y,children:[(0,W.jsx)(ge,{size:15}),` `,`Previous page`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>void m(),disabled:!b,children:[o?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(_e,{size:15}),` `,`Next page`]})]}),u&&(0,W.jsx)(er,{onClose:()=>d(!1),onCreated:()=>void p()})]})}function rr({navigate:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)(``);(0,g.useEffect)(()=>{let e=!1;async function t(){try{let t=await k.dashboard();e||n(t)}catch(t){e||i(t instanceof Error?t.message:`Failed to load dashboard`)}}t();let r=window.setInterval(()=>void t(),15e3);return()=>{e=!0,window.clearInterval(r)}},[]);let a=t?.counts,o=t?.storage,s=t?.host;return(0,W.jsxs)(`div`,{className:`dashboard-layout`,children:[r&&(0,W.jsx)(K,{children:r}),(0,W.jsxs)(ir,{title:`Needs attention`,children:[(0,W.jsx)(ar,{icon:(0,W.jsx)(we,{}),label:`Pending reports`,value:a?_t(String(a.PendingReports)):`…`,tone:a&&a.PendingReports>0?`warn`:`good`,href:`/moderation`,navigate:e}),(0,W.jsx)(ar,{icon:(0,W.jsx)(F,{}),label:`Verification requests`,value:a?_t(String(a.PendingVerifications)):`…`,tone:a&&a.PendingVerifications>0?`warn`:`good`,href:`/verification`,navigate:e})]}),(0,W.jsxs)(ir,{title:`People & chats`,children:[(0,W.jsx)(ar,{icon:(0,W.jsx)(ct,{}),label:`Users`,value:a?_t(String(a.Users)):`…`,href:`/accounts`,navigate:e}),(0,W.jsx)(ar,{icon:(0,W.jsx)(se,{}),label:`Online now`,value:a?_t(String(a.OnlineUsers)):`…`,sub:`last 5 min`,href:`/accounts`,navigate:e}),(0,W.jsx)(ar,{icon:(0,W.jsx)(R,{}),label:`Bots`,value:a?_t(String(a.Bots)):`…`,href:`/bots`,navigate:e}),(0,W.jsx)(ar,{icon:(0,W.jsx)(Ge,{}),label:`Channels`,value:a?_t(String(a.BroadcastChannels)):`…`,href:`/channels`,navigate:e}),(0,W.jsx)(ar,{icon:(0,W.jsx)(oe,{}),label:`Supergroups`,value:a?_t(String(a.Supergroups)):`…`,href:`/channels`,navigate:e})]}),(0,W.jsxs)(ir,{title:`Content`,children:[(0,W.jsx)(ar,{icon:(0,W.jsx)(nt,{}),label:`Sticker packs`,value:a?_t(String(a.StickerSets)):`…`,href:`/stickers`,navigate:e}),(0,W.jsx)(ar,{icon:(0,W.jsx)(et,{}),label:`Emoji packs`,value:a?_t(String(a.EmojiSets)):`…`,href:`/emoji`,navigate:e}),(0,W.jsx)(ar,{icon:(0,W.jsx)(Ce,{}),label:`GIFs`,value:a?_t(String(a.Gifs)):`…`,sub:`saved by users`,href:`/gif-catalog`,navigate:e}),(0,W.jsx)(ar,{icon:(0,W.jsx)(be,{}),label:`Media storage used`,value:o?wt(o.PhysicalBytes):`…`,sub:o?`${o.BackendKind} backend`:void 0,href:`/storage`,navigate:e})]}),(0,W.jsxs)(ir,{title:`Server health`,hint:s?.Ready?void 0:`waiting for first sample…`,children:[(0,W.jsx)(or,{icon:(0,W.jsx)(ye,{}),label:`CPU load`,percent:s?.Ready?s.CPUPercent:void 0,valueText:s?.Ready?`${s.CPUPercent.toFixed(0)}%`:`…`}),(0,W.jsx)(or,{icon:(0,W.jsx)(Ie,{}),label:`RAM used`,percent:s?.Ready&&s.MemTotalBytes>0?s.MemUsedBytes/s.MemTotalBytes*100:void 0,valueText:s?.Ready?wt(String(s.MemUsedBytes)):`…`,sub:s?.Ready?`of ${wt(String(s.MemTotalBytes))}`:void 0}),(0,W.jsx)(or,{icon:(0,W.jsx)(De,{}),label:`Disk free`,percent:s?.Ready&&s.DiskTotalBytes>0?(s.DiskTotalBytes-s.DiskFreeBytes)/s.DiskTotalBytes*100:void 0,valueText:s?.Ready?wt(String(s.DiskFreeBytes)):`…`,sub:s?.Ready?`of ${wt(String(s.DiskTotalBytes))}`:void 0,warnAbove:85})]})]})}function ir({title:e,hint:t,children:n}){return(0,W.jsxs)(`div`,{className:`dashboard-section`,children:[(0,W.jsxs)(`div`,{className:`dashboard-section-title`,children:[e,t&&(0,W.jsx)(`span`,{children:t})]}),(0,W.jsx)(`div`,{className:`dashboard-grid`,children:n})]})}function ar({icon:e,label:t,value:n,sub:r,tone:i=`neutral`,href:a,navigate:o}){let s=i===`neutral`?``:` ${i}`,c=(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`div`,{className:`stat-tile-head`,children:[(0,W.jsx)(`span`,{className:`stat-tile-icon`,children:e}),i===`warn`&&(0,W.jsx)(ie,{size:15,className:`stat-tile-open`})]}),(0,W.jsx)(`div`,{className:`stat-tile-value`,children:n}),(0,W.jsx)(`div`,{className:`stat-tile-label`,children:t}),r&&(0,W.jsx)(`div`,{className:`stat-tile-sub`,children:r})]});return a&&o?(0,W.jsx)(`a`,{className:`stat-tile clickable${s}`,href:a,onClick:e=>{e.preventDefault(),o(a)},children:c}):(0,W.jsx)(`div`,{className:`stat-tile${s}`,children:c})}function or({icon:e,label:t,percent:n,valueText:r,sub:i,warnAbove:a=90}){let o=n===void 0?0:Math.max(0,Math.min(100,n)),s=n===void 0?`neutral`:n>=a?`danger`:n>=a-15?`warn`:`neutral`;return(0,W.jsxs)(`div`,{className:`stat-tile${s===`neutral`?``:` ${s}`}`,children:[(0,W.jsx)(`div`,{className:`stat-tile-head`,children:(0,W.jsx)(`span`,{className:`stat-tile-icon`,children:e})}),(0,W.jsx)(`div`,{className:`stat-tile-value`,children:r}),(0,W.jsx)(`div`,{className:`stat-tile-label`,children:t}),i&&(0,W.jsx)(`div`,{className:`stat-tile-sub`,children:i}),(0,W.jsx)(`div`,{className:`stat-tile-bar`,children:(0,W.jsx)(`span`,{style:{width:`${o}%`}})})]})}function sr({channelID:e,msgID:t,navigate:n}){let[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``);async function s(){o(``);try{i(await k.groupMessage(e,t))}catch(e){o(O(e))}}if((0,g.useEffect)(()=>{s()},[e,t]),a)return(0,W.jsx)(K,{children:a});if(!r)return(0,W.jsx)(X,{label:`Loading`});let c=r.Message;return(0,W.jsx)(Dt,{title:`Group Message #${c.ID}`,eyebrow:`Message Detail`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,W.jsx)(le,{size:15}),` `,`Back to group messages`]}),children:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:`Channel / Group ${c.ChannelID}`}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`Sender ${c.SenderUserID} · ${mt(c.Date)}`})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[c.Deleted?(0,W.jsx)(q,{tone:`danger`,children:`Deleted`}):(0,W.jsx)(q,{children:`Live`}),c.Pinned&&(0,W.jsx)(q,{tone:`warn`,children:`Pinned`}),c.Post&&(0,W.jsx)(q,{children:`Channel post`}),(0,W.jsxs)(q,{children:[`pts `,c.PTS]})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Message ID`,value:String(c.ID),mono:!0}),(0,W.jsx)(Y,{label:`Channel / Group`,value:String(c.ChannelID),mono:!0}),(0,W.jsx)(Y,{label:`From Peer`,value:`${c.FromPeerType}:${c.FromPeerID}`,mono:!0}),(0,W.jsx)(Y,{label:`Views`,value:String(c.ViewsCount)})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Channel Message Row`,text:`channel_messages read-only snapshot`}),(0,W.jsx)(Mt,{value:r.MessageJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Channel Row`,text:`channels read-only snapshot`}),(0,W.jsx)(Mt,{value:r.ChannelJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Channel Update Events`,text:`durable channel_update_events`}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Count`}),(0,W.jsx)(`th`,{children:`Type`}),(0,W.jsx)(`th`,{children:`Message ID`}),(0,W.jsx)(`th`,{children:`Sender`}),(0,W.jsx)(`th`,{children:`Time`})]})}),(0,W.jsxs)(`tbody`,{children:[r.UpdateEvents.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.PTSCount}),(0,W.jsx)(`td`,{children:e.Type}),(0,W.jsx)(`td`,{children:e.MessageID}),(0,W.jsx)(`td`,{children:e.SenderUserID}),(0,W.jsx)(`td`,{children:mt(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),r.UpdateEvents.length===0&&(0,W.jsx)(jt,{colSpan:6})]})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Event JSON`}),(0,W.jsxs)(`div`,{className:`raw-grid`,children:[r.UpdateEvents.map(e=>(0,W.jsx)(Mt,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),r.UpdateEvents.length===0&&(0,W.jsx)(`div`,{className:`empty-panel`,children:`No results`})]})]})]})})}function cr({navigate:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(`100`),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(``);async function p(e=!1){if(f(``),!t){f(`Search and select a supergroup or channel first`);return}let n=new URLSearchParams({channel_id:String(t.ID),limit:s});if(e&&l?.rows.length){let e=l.rows[l.rows.length-1];n.set(`before_date`,String(e.Date)),n.set(`before_id`,String(e.ID)),i(String(e.Date)),o(String(e.ID))}else r&&n.set(`before_date`,r),a&&n.set(`before_id`,a);try{u(await k.groupMessages(n))}catch(e){f(O(e))}}function m(e){n(e),i(``),o(``),u(null)}let h=l?.rows??[];return(0,W.jsxs)(Dt,{title:`Group Messages`,eyebrow:`Supergroup / channel messages`,children:[d&&(0,W.jsx)(K,{children:d}),(0,W.jsxs)(Ot,{children:[(0,W.jsx)(`div`,{className:`message-selector-grid single`,children:(0,W.jsx)(Pn,{label:`Channel / Group`,value:t,onChange:m})}),(0,W.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),p(!1)},children:[(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`before_date cursor`}),(0,W.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`before_msg_id cursor`}),(0,W.jsx)(`input`,{className:`small-input`,value:s,onChange:e=>c(e.target.value),placeholder:`limit <= 100`}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,W.jsx)(V,{size:15}),` `,`Search messages`]}),h.length?(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>p(!0),children:[(0,W.jsx)(_e,{size:15}),` `,`Next page`]}):null]})]}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Messages on page`,value:String(h.length)}),(0,W.jsx)(J,{label:`With media`,value:String(h.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,W.jsx)(J,{label:`Channel posts`,value:String(h.filter(e=>e.Post).length)}),(0,W.jsx)(J,{label:`Channel / Group`,value:t?`${t.Title||pt(t)} (${t.ID})`:`-`})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Message ID`}),(0,W.jsx)(`th`,{children:`Time`}),(0,W.jsx)(`th`,{children:`Sender`}),(0,W.jsx)(`th`,{children:`From Peer`}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Views`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Body`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[h.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:t.ID}),(0,W.jsx)(`td`,{children:mt(t.Date)}),(0,W.jsx)(`td`,{className:`mono`,children:t.SenderUserID}),(0,W.jsxs)(`td`,{className:`mono`,children:[t.FromPeerType,`:`,t.FromPeerID]}),(0,W.jsx)(`td`,{children:t.PTS}),(0,W.jsx)(`td`,{children:t.ViewsCount}),(0,W.jsx)(`td`,{children:t.Deleted?(0,W.jsx)(q,{tone:`danger`,children:`Deleted`}):t.Pinned?(0,W.jsx)(q,{tone:`warn`,children:`Pinned`}):(0,W.jsx)(q,{children:`Live`})}),(0,W.jsx)(`td`,{className:`truncate`,children:t.Body}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${t.ChannelID}&msg_id=${t.ID}`),children:[`Details`,` `,(0,W.jsx)(_e,{size:14})]})})]},`${t.ChannelID}-${t.ID}`)),h.length===0&&(0,W.jsx)(jt,{colSpan:9})]})]})})]})}function lr({ownerUserID:e,msgID:t,navigate:n}){let[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``);async function s(){o(``);try{i(await k.message(e,t))}catch(e){o(O(e))}}if((0,g.useEffect)(()=>{s()},[e,t]),a)return(0,W.jsx)(K,{children:a});if(!r)return(0,W.jsx)(X,{label:`Loading`});let c=r.Message;return(0,W.jsx)(Dt,{title:`Message #${c.BoxID}`,eyebrow:`Message Detail`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,W.jsx)(le,{size:15}),` `,`Back to private messages`]}),children:(0,W.jsx)(kt,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:`Owner ${c.OwnerUserID} · Peer ${c.PeerID}`}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`Sender ${c.FromUserID} · ${mt(c.Date)}`})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[c.Deleted?(0,W.jsx)(q,{tone:`danger`,children:`Deleted`}):(0,W.jsx)(q,{children:`Live`}),(0,W.jsxs)(q,{children:[`pts `,c.PTS]}),(0,W.jsx)(q,{children:c.Outgoing?`Outgoing`:`Incoming`})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Message box ID`,value:String(c.BoxID),mono:!0}),(0,W.jsx)(Y,{label:`Private message ID`,value:String(c.PrivateMessageID),mono:!0}),(0,W.jsx)(Y,{label:`Message sender`,value:String(c.MessageSenderID),mono:!0}),(0,W.jsx)(Y,{label:`Time`,value:mt(c.Date)})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Message Box`,text:`message_boxes read-only snapshot`}),(0,W.jsx)(Mt,{value:r.MessageJSON})]}),(0,W.jsxs)(`div`,{className:`raw-grid`,children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Dialog Row`,text:`dialogs read-only snapshot`}),(0,W.jsx)(Mt,{value:r.DialogJSON})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Private Message Row`,text:`private_messages read-only snapshot`}),(0,W.jsx)(Mt,{value:r.PrivateJSON})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Update Events`,text:`durable user_update_events`}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Count`}),(0,W.jsx)(`th`,{children:`Type`}),(0,W.jsx)(`th`,{children:`Time`})]})}),(0,W.jsxs)(`tbody`,{children:[r.UpdateEvents.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.PTSCount}),(0,W.jsx)(`td`,{children:e.Type}),(0,W.jsx)(`td`,{children:mt(e.Date)})]},`${e.PTS}-${e.Type}`)),r.UpdateEvents.length===0&&(0,W.jsx)(jt,{colSpan:4})]})]})})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Dispatch Queue`,text:`online/offline dispatch_outbox`}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`User ID`}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Type`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Attempts`}),(0,W.jsx)(`th`,{children:`Updated`})]})}),(0,W.jsxs)(`tbody`,{children:[r.Outbox.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:e.ID}),(0,W.jsx)(`td`,{children:e.TargetUserID}),(0,W.jsx)(`td`,{children:e.PTS}),(0,W.jsx)(`td`,{children:e.EventType}),(0,W.jsx)(`td`,{children:e.Status}),(0,W.jsx)(`td`,{children:e.Attempts}),(0,W.jsx)(`td`,{children:U(e.UpdatedAt)})]},e.ID)),r.Outbox.length===0&&(0,W.jsx)(jt,{colSpan:7})]})]})})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:`Operations`}),(0,W.jsx)(Z,{label:`Delete this message`,icon:(0,W.jsx)(it,{size:15}),path:`/api/actions/delete-messages`,payload:()=>({owner_user_id:c.OwnerUserID,peer_id:c.PeerID,ids:[c.BoxID],revoke:!0}),onDone:s})]})})})}function ur({navigate:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(`100`),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(!0),[h,_]=(0,g.useState)(!1),[v,y]=(0,g.useState)(``),[b,x]=(0,g.useState)(`1`),[S,C]=(0,g.useState)(null),[w,T]=(0,g.useState)(``);async function E(e=!1){if(T(``),!t||!r){T(`Search and select the owner user and peer user first`);return}let n=new URLSearchParams({owner_user_id:String(t.ID),peer_id:String(r.ID),limit:l});if(e&&S?.rows.length){let e=S.rows[S.rows.length-1];n.set(`before_date`,String(e.Date)),n.set(`before_id`,String(e.BoxID)),o(String(e.Date)),c(String(e.BoxID))}else a&&n.set(`before_date`,a),s&&n.set(`before_id`,s);try{C(await k.messages(n))}catch(e){T(O(e))}}function D(e){n(e),o(``),c(``),C(null)}function A(e){i(e),o(``),c(``),C(null)}return(0,W.jsxs)(Dt,{title:`Private Messages`,eyebrow:`Private message boxes`,children:[w&&(0,W.jsx)(K,{children:w}),(0,W.jsxs)(Ot,{children:[(0,W.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,W.jsx)(jn,{label:`Owner user`,value:t,onChange:D}),(0,W.jsx)(jn,{label:`Peer user`,value:r,onChange:A})]}),(0,W.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),E(!1)},children:[(0,W.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`before_date cursor`}),(0,W.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`before_msg_id cursor`}),(0,W.jsx)(`input`,{className:`small-input`,value:l,onChange:e=>u(e.target.value),placeholder:`limit <= 100`}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,W.jsx)(V,{size:15}),` `,`Search messages`]}),S?.rows.length?(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>E(!0),children:[(0,W.jsx)(_e,{size:15}),` `,`Next page`]}):null]})]}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Messages on page`,value:String(S?.rows.length??0)}),(0,W.jsx)(J,{label:`Deleted`,value:String((S?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,W.jsx)(J,{label:`Outgoing`,value:String((S?.rows??[]).filter(e=>e.Outgoing).length)}),(0,W.jsx)(J,{label:`Owner / Peer`,value:t&&r?`${ft(t)} / ${ft(r)}`:`-`})]}),(0,W.jsxs)(`div`,{className:`operation-row`,children:[(0,W.jsxs)(`div`,{className:`operation-box`,children:[(0,W.jsxs)(`div`,{className:`operation-title`,children:[(0,W.jsx)(it,{size:15}),` `,`Delete selected messages`]}),(0,W.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),placeholder:`Message IDs, comma separated`}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>m(e.target.checked)}),` `,`Revoke for both sides`]}),(0,W.jsx)(Z,{path:`/api/actions/delete-messages`,label:`Dry-run delete`,payload:()=>({owner_user_id:t?.ID??0,peer_id:r?.ID??0,ids:Tt(d,`Message IDs are invalid`),revoke:p})})]}),(0,W.jsxs)(`div`,{className:`operation-box`,children:[(0,W.jsxs)(`div`,{className:`operation-title`,children:[(0,W.jsx)(Oe,{size:15}),` `,`Clear private history`]}),(0,W.jsx)(`input`,{value:v,onChange:e=>y(e.target.value),placeholder:`max_id cutoff`}),(0,W.jsx)(`input`,{value:b,onChange:e=>x(e.target.value),placeholder:`max_batches`}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>m(e.target.checked)}),` `,`Revoke for both sides`]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:h,onChange:e=>_(e.target.checked)}),` `,`Clear only this side`]}),(0,W.jsx)(Z,{path:`/api/actions/delete-history`,label:`Dry-run clear history`,payload:()=>({owner_user_id:t?.ID??0,peer_id:r?.ID??0,max_id:gt(v),max_batches:gt(b),just_clear:h,revoke:p})})]})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Message ID`}),(0,W.jsx)(`th`,{children:`Time`}),(0,W.jsx)(`th`,{children:`Sender`}),(0,W.jsx)(`th`,{children:`Direction`}),(0,W.jsx)(`th`,{children:`PTS`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Body`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[S?.rows.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:t.BoxID}),(0,W.jsx)(`td`,{children:mt(t.Date)}),(0,W.jsx)(`td`,{className:`mono`,children:t.FromUserID}),(0,W.jsx)(`td`,{children:t.Outgoing?`Outgoing`:`Incoming`}),(0,W.jsx)(`td`,{children:t.PTS}),(0,W.jsx)(`td`,{children:t.Deleted?(0,W.jsx)(q,{tone:`danger`,children:`Deleted`}):(0,W.jsx)(q,{children:`Live`})}),(0,W.jsx)(`td`,{className:`truncate`,children:t.Body}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${t.OwnerUserID}&msg_id=${t.BoxID}`),children:[`Details`,` `,(0,W.jsx)(_e,{size:14})]})})]},`${t.OwnerUserID}-${t.BoxID}`)),(!S||S.rows.length===0)&&(0,W.jsx)(jt,{colSpan:8})]})]})})]})}var dr=c(o(((e,t)=>{typeof document<`u`&&typeof navigator<`u`&&(function(n,r){typeof e==`object`&&t!==void 0?t.exports=r():typeof define==`function`&&define.amd?define(r):(n=typeof globalThis<`u`?globalThis:n||self,n.lottie=r())})(e,(function(){var n=``,r=!1,i=-999999,a=function(e){r=!!e},o=function(){return r},s=function(e){n=e},c=function(){return n};function l(e){return document.createElement(e)}function u(e,t){var n,r=e.length,i;for(n=0;n1?n[1]=1:n[1]<=0&&(n[1]=0),I(n[0],n[1],n[2])}function ne(e,t){var n=te(e[0]*255,e[1]*255,e[2]*255);return n[2]+=t,n[2]>1?n[2]=1:n[2]<0&&(n[2]=0),I(n[0],n[1],n[2])}function re(e,t){var n=te(e[0]*255,e[1]*255,e[2]*255);return n[0]+=t/360,n[0]>1?--n[0]:n[0]<0&&(n[0]+=1),I(n[0],n[1],n[2])}(function(){var e=[],t,n;for(t=0;t<256;t+=1)n=t.toString(16),e[t]=n.length===1?`0`+n:n;return function(t,n,r){return t<0&&(t=0),n<0&&(n=0),r<0&&(r=0),`#`+e[t]+e[n]+e[r]}})();var ie=function(e){g=!!e},ae=function(){return g},oe=function(e){_=e},se=function(){return _},ce=function(){return v},le=function(e){E=e},ue=function(){return E},de=function(e){y=e};function R(e){return document.createElementNS(`http://www.w3.org/2000/svg`,e)}function fe(e){"@babel/helpers - typeof";return fe=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},fe(e)}var pe=function(){var e=1,t=[],n,r,i={onmessage:function(){},postMessage:function(e){n({data:e})}},a={postMessage:function(e){i.onmessage({data:e})}};function s(e){if(window.Worker&&window.Blob&&o()){var t=new Blob([`var _workerSelf = self; self.onmessage = `,e.toString()],{type:`text/javascript`}),r=URL.createObjectURL(t);return new Worker(r)}return n=e,i}function c(){r||(r=s(function(e){function t(){function e(t,n){var o,s,c=t.length,l,u,d,f;for(s=0;s=0;--t)if(e[t].ty===`sh`)if(e[t].ks.k.i)a(e[t].ks.k);else for(o=e[t].ks.k.length,r=0;rn[0]?!0:n[0]>e[0]?!1:e[1]>n[1]?!0:n[1]>e[1]?!1:e[2]>n[2]?!0:n[2]>e[2]?!1:null}var s=function(){var e=[4,4,14];function t(e){var t=e.t.d;e.t.d={k:[{s:t,t:0}]}}function n(e){var n,r=e.length;for(n=0;n=0;--n)if(e[n].ty===`sh`)if(e[n].ks.k.i)e[n].ks.k.c=e[n].closed;else for(a=e[n].ks.k.length,i=0;i500)&&(this._imageLoaded(),clearInterval(n)),t+=1}.bind(this),50)}function a(t){var n=r(t,this.assetsPath,this.path),i=R(`image`);b?this.testImageLoaded(i):i.addEventListener(`load`,this._imageLoaded,!1),i.addEventListener(`error`,function(){a.img=e,this._imageLoaded()}.bind(this),!1),i.setAttributeNS(`http://www.w3.org/1999/xlink`,`href`,n),this._elementHelper.append?this._elementHelper.append(i):this._elementHelper.appendChild(i);var a={img:i,assetData:t};return a}function o(t){var n=r(t,this.assetsPath,this.path),i=l(`img`);i.crossOrigin=`anonymous`,i.addEventListener(`load`,this._imageLoaded,!1),i.addEventListener(`error`,function(){a.img=e,this._imageLoaded()}.bind(this),!1),i.src=n;var a={img:i,assetData:t};return a}function s(e){var t={assetData:e},n=r(e,this.assetsPath,this.path);return pe.loadData(n,function(e){t.img=e,this._footageLoaded()}.bind(this),function(){t.img={},this._footageLoaded()}.bind(this)),t}function c(e,t){this.imagesLoadedCb=t;var n,r=e.length;for(n=0;nthis.animationData.op&&(this.animationData.op=e.op,this.totalFrames=Math.floor(e.op-this.animationData.ip));var t=this.animationData.layers,n,r=t.length,i=e.layers,a,o=i.length;for(a=0;athis.timeCompleted&&(this.currentFrame=this.timeCompleted),this.trigger(`enterFrame`),this.renderFrame(),this.trigger(`drawnFrame`)},z.prototype.renderFrame=function(){if(!(this.isLoaded===!1||!this.renderer))try{this.expressionsPlugin&&this.expressionsPlugin.resetFrame(),this.renderer.renderFrame(this.currentFrame+this.firstFrame)}catch(e){this.triggerRenderFrameError(e)}},z.prototype.play=function(e){e&&this.name!==e||this.isPaused===!0&&(this.isPaused=!1,this.trigger(`_play`),this.audioController.resume(),this._idle&&(this._idle=!1,this.trigger(`_active`)))},z.prototype.pause=function(e){e&&this.name!==e||this.isPaused===!1&&(this.isPaused=!0,this.trigger(`_pause`),this._idle=!0,this.trigger(`_idle`),this.audioController.pause())},z.prototype.togglePause=function(e){e&&this.name!==e||(this.isPaused===!0?this.play():this.pause())},z.prototype.stop=function(e){e&&this.name!==e||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},z.prototype.getMarkerData=function(e){for(var t,n=0;n=this.totalFrames-1&&this.frameModifier>0?!this.loop||this.playCount===this.loop?this.checkSegments(t>this.totalFrames?t%this.totalFrames:0)||(n=!0,t=this.totalFrames-1):t>=this.totalFrames?(this.playCount+=1,this.checkSegments(t%this.totalFrames)||(this.setCurrentRawFrameValue(t%this.totalFrames),this._completedLoop=!0,this.trigger(`loopComplete`))):this.setCurrentRawFrameValue(t):t<0?this.checkSegments(t%this.totalFrames)||(this.loop&&!(this.playCount--<=0&&this.loop!==!0)?(this.setCurrentRawFrameValue(this.totalFrames+t%this.totalFrames),this._completedLoop?this.trigger(`loopComplete`):this._completedLoop=!0):(n=!0,t=0)):this.setCurrentRawFrameValue(t),n&&(this.setCurrentRawFrameValue(t),this.pause(),this.trigger(`complete`))}},z.prototype.adjustSegment=function(e,t){this.playCount=0,e[1]0&&(this.playSpeed<0?this.setSpeed(-this.playSpeed):this.setDirection(-1)),this.totalFrames=e[0]-e[1],this.timeCompleted=this.totalFrames,this.firstFrame=e[1],this.setCurrentRawFrameValue(this.totalFrames-.001-t)):e[1]>e[0]&&(this.frameModifier<0&&(this.playSpeed<0?this.setSpeed(-this.playSpeed):this.setDirection(1)),this.totalFrames=e[1]-e[0],this.timeCompleted=this.totalFrames,this.firstFrame=e[0],this.setCurrentRawFrameValue(.001+t)),this.trigger(`segmentStart`)},z.prototype.setSegment=function(e,t){var n=-1;this.isPaused&&(this.currentRawFrame+this.firstFramet&&(n=t-e)),this.firstFrame=e,this.totalFrames=t-e,this.timeCompleted=this.totalFrames,n!==-1&&this.goToAndStop(n,!0)},z.prototype.playSegments=function(e,t){if(t&&(this.segments.length=0),Se(e[0])===`object`){var n,r=e.length;for(n=0;n=0;--n)t[n].animation.destroy(e)}function T(e,t,n){var r=[].concat([].slice.call(document.getElementsByClassName(`lottie`)),[].slice.call(document.getElementsByClassName(`bodymovin`))),i,a=r.length;for(i=0;i0?n=c:t=c;while(Math.abs(s)>a&&++l=i?g(e,d,t,n):f===0?d:h(e,a,a+c,t,n)}},e}(),Te=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),Ee=function(){return function(e,t,n){var r=0,i=e,a=m(i),o={newElement:s,release:c};function s(){var e;return r?(--r,e=a[r]):e=t(),e}function c(e){r===i&&(a=Te.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),De=function(){function e(){return{addedLength:0,percents:p(`float32`,ue()),lengths:p(`float32`,ue())}}return Ee(8,e)}(),Oe=function(){function e(){return{lengths:[],totalLength:0}}function t(e){var t,n=e.lengths.length;for(t=0;t-.001&&o<.001}function n(n,r,i,a,o,s,c,l,u){if(i===0&&s===0&&u===0)return t(n,r,a,o,c,l);var d=e.sqrt(e.pow(a-n,2)+e.pow(o-r,2)+e.pow(s-i,2)),f=e.sqrt(e.pow(c-n,2)+e.pow(l-r,2)+e.pow(u-i,2)),p=e.sqrt(e.pow(c-a,2)+e.pow(l-o,2)+e.pow(u-s,2)),m=d>f?d>p?d-f-p:p-f-d:p>f?p-f-d:f-d-p;return m>-1e-4&&m<1e-4}var r=function(){return function(e,t,n,r){var i=ue(),a,o,s,c,l,u=0,d,f=[],p=[],m=De.newElement();for(s=n.length,a=0;ao?-1:1,l=!0;l;)if(r[a]<=o&&r[a+1]>o?(s=(o-r[a])/(r[a+1]-r[a]),l=!1):a+=c,a<0||a>=i-1){if(a===i-1)return n[a];l=!1}return n[a]+(n[a+1]-n[a])*s}function l(t,n,r,i,a,o){var s=c(a,o),l=1-s;return[e.round((l*l*l*t[0]+(s*l*l+l*s*l+l*l*s)*r[0]+(s*s*l+l*s*s+s*l*s)*i[0]+s*s*s*n[0])*1e3)/1e3,e.round((l*l*l*t[1]+(s*l*l+l*s*l+l*l*s)*r[1]+(s*s*l+l*s*s+s*l*s)*i[1]+s*s*s*n[1])*1e3)/1e3]}var u=p(`float32`,8);function d(t,n,r,i,a,o,s){a<0?a=0:a>1&&(a=1);var l=c(a,s);o=o>1?1:o;var d=c(o,s),f,p=t.length,m=1-l,h=1-d,g=m*m*m,_=l*m*m*3,v=l*l*m*3,y=l*l*l,b=m*m*h,x=l*m*h+m*l*h+m*m*d,S=l*l*h+m*l*d+l*m*d,C=l*l*d,w=m*h*h,T=l*h*h+m*d*h+m*h*d,E=l*d*h+m*d*d+l*h*d,D=l*d*d,O=h*h*h,k=d*h*h+h*d*h+h*h*d,A=d*d*h+h*d*d+d*h*d,j=d*d*d;for(f=0;f=l.t-n){c.h&&(c=l),i=0;break}if(l.t-n>e){i=a;break}a=v||e=v?x.points.length-1:0;for(f=x.points[S].point.length,d=0;d=T&&C=v)r[0]=b[0],r[1]=b[1],r[2]=b[2];else if(e<=y)r[0]=c.s[0],r[1]=c.s[1],r[2]=c.s[2];else{var j=Ie(c.s),M=Ie(b),N=(e-y)/(v-y);Fe(r,Pe(j,M,N))}else for(a=0;a=v?m=1:e1e-6?(f=Math.acos(p),m=Math.sin(f),h=Math.sin((1-n)*f)/m,g=Math.sin(n*f)/m):(h=1-n,g=n),r[0]=h*i+g*c,r[1]=h*a+g*l,r[2]=h*o+g*u,r[3]=h*s+g*d,r}function Fe(e,t){var n=t[0],r=t[1],i=t[2],a=t[3],o=Math.atan2(2*r*a-2*n*i,1-2*r*r-2*i*i),s=Math.asin(2*n*r+2*i*a),c=Math.atan2(2*n*a-2*r*i,1-2*n*n-2*i*i);e[0]=o/D,e[1]=s/D,e[2]=c/D}function Ie(e){var t=e[0]*D,n=e[1]*D,r=e[2]*D,i=Math.cos(t/2),a=Math.cos(n/2),o=Math.cos(r/2),s=Math.sin(t/2),c=Math.sin(n/2),l=Math.sin(r/2),u=i*a*o-s*c*l;return[s*c*o+i*a*l,s*a*o+i*c*l,i*c*o-s*a*l,u]}function Le(){var e=this.comp.renderedFrame-this.offsetTime,t=this.keyframes[0].t-this.offsetTime,n=this.keyframes[this.keyframes.length-1].t-this.offsetTime;if(!(e===this._caching.lastFrame||this._caching.lastFrame!==je&&(this._caching.lastFrame>=n&&e>=n||this._caching.lastFrame=e&&(this._caching._lastKeyframeIndex=-1,this._caching.lastIndex=0);var r=this.interpolateValue(e,this._caching);this.pv=r}return this._caching.lastFrame=e,this.pv}function Re(e){var t;if(this.propType===`unidimensional`)t=e*this.mult,Me(this.v-t)>1e-5&&(this.v=t,this._mdf=!0);else for(var n=0,r=this.v.length;n1e-5&&(this.v[n]=t,this._mdf=!0),n+=1}function ze(){if(!(this.elem.globalData.frameId===this.frameId||!this.effectsSequence.length)){if(this.lock){this.setVValue(this.pv);return}this.lock=!0,this._mdf=this._isFirstFrame;var e,t=this.effectsSequence.length,n=this.kf?this.pv:this.data.k;for(e=0;e=this._maxLength&&this.doubleArrayLength(),n){case`v`:a=this.v;break;case`i`:a=this.i;break;case`o`:a=this.o;break;default:a=[];break}(!a[r]||a[r]&&!i)&&(a[r]=Ke.newElement()),a[r][0]=e,a[r][1]=t},qe.prototype.setTripleAt=function(e,t,n,r,i,a,o,s){this.setXYAt(e,t,`v`,o,s),this.setXYAt(n,r,`o`,o,s),this.setXYAt(i,a,`i`,o,s)},qe.prototype.reverse=function(){var e=new qe;e.setPathData(this.c,this._length);var t=this.v,n=this.o,r=this.i,i=0;this.c&&(e.setTripleAt(t[0][0],t[0][1],r[0][0],r[0][1],n[0][0],n[0][1],0,!1),i=1);var a=this._length-1,o=this._length,s;for(s=i;s=p[p.length-1].t-this.offsetTime)i=p[p.length-1].s?p[p.length-1].s[0]:p[p.length-2].e[0],o=!0;else{for(var m=r,h=p.length-1,g=!0,_,v,y;g&&(_=p[m],v=p[m+1],!(v.t-this.offsetTime>e));)m=v.t-this.offsetTime)d=1;else if(e<_.t-this.offsetTime)d=0;else{var b;y.__fnct?b=y.__fnct:(b=we.getBezierEasing(_.o.x,_.o.y,_.i.x,_.i.y).get,y.__fnct=b),d=b((e-(_.t-this.offsetTime))/(v.t-this.offsetTime-(_.t-this.offsetTime)))}a=v.s?v.s[0]:_.e[0]}i=_.s[0]}for(l=t._length,u=i.i[0].length,n.lastIndex=r,s=0;sr&&t>r)||(this._caching.lastIndex=i0||e>-1e-6&&e<0?r(e*t)/t:e}function P(){var e=this.props,t=N(e[0]),n=N(e[1]),r=N(e[4]),i=N(e[5]),a=N(e[12]),o=N(e[13]);return`matrix(`+t+`,`+n+`,`+r+`,`+i+`,`+a+`,`+o+`)`}return function(){this.reset=i,this.rotate=a,this.rotateX=o,this.rotateY=s,this.rotateZ=c,this.skew=u,this.skewFromAxis=d,this.shear=l,this.scale=f,this.setTransform=m,this.translate=h,this.transform=g,this.multiply=_,this.applyToPoint=S,this.applyToX=C,this.applyToY=w,this.applyToZ=T,this.applyToPointArray=A,this.applyToTriplePoints=k,this.applyToPointStringified=j,this.toCSS=M,this.to2dCSS=P,this.clone=b,this.cloneFromProps=x,this.equals=y,this.inversePoints=O,this.inversePoint=D,this.getInverseMatrix=E,this._t=this.transform,this.isIdentity=v,this._identity=!0,this._identityCalculated=!1,this.props=p(`float32`,16),this.reset()}}();function Qe(e){"@babel/helpers - typeof";return Qe=typeof Symbol==`function`&&typeof Symbol.iterator==`symbol`?function(e){return typeof e}:function(e){return e&&typeof Symbol==`function`&&e.constructor===Symbol&&e!==Symbol.prototype?`symbol`:typeof e},Qe(e)}var $e={},et=`__[STANDALONE]__`,tt=`__[ANIMATIONDATA]__`,nt=``;function rt(e){s(e)}function it(){et===!0?Ce.searchAnimations(tt,et,nt):Ce.searchAnimations()}function at(e){ie(e)}function ot(e){de(e)}function st(e){return et===!0&&(e.animationData=JSON.parse(tt)),Ce.loadAnimation(e)}function ct(e){if(typeof e==`string`)switch(e){case`high`:le(200);break;default:case`medium`:le(50);break;case`low`:le(10);break}else!isNaN(e)&&e>1&&le(e)}function lt(){return typeof navigator<`u`}function ut(e,t){e===`expressions`&&oe(t)}function dt(e){switch(e){case`propertyFactory`:return B;case`shapePropertyFactory`:return Xe;case`matrix`:return Ze;default:return null}}$e.play=Ce.play,$e.pause=Ce.pause,$e.setLocationHref=rt,$e.togglePause=Ce.togglePause,$e.setSpeed=Ce.setSpeed,$e.setDirection=Ce.setDirection,$e.stop=Ce.stop,$e.searchAnimations=it,$e.registerAnimation=Ce.registerAnimation,$e.loadAnimation=st,$e.setSubframeRendering=at,$e.resize=Ce.resize,$e.goToAndStop=Ce.goToAndStop,$e.destroy=Ce.destroy,$e.setQuality=ct,$e.inBrowser=lt,$e.installPlugin=ut,$e.freeze=Ce.freeze,$e.unfreeze=Ce.unfreeze,$e.setVolume=Ce.setVolume,$e.mute=Ce.mute,$e.unmute=Ce.unmute,$e.getRegisteredAnimations=Ce.getRegisteredAnimations,$e.useWebWorker=a,$e.setIDPrefix=ot,$e.__getFactory=dt,$e.version=`5.13.0`;function H(){document.readyState===`complete`&&(clearInterval(ht),it())}function ft(e){for(var t=pt.split(`&`),n=0;n=1?a.push({s:e-1,e:t-1}):(a.push({s:e,e:1}),a.push({s:0,e:t-1}));var o=[],s,c=a.length,l;for(s=0;sr+n)){var u=l.s*i<=r?0:(l.s*i-r)/n,d=l.e*i>=r+n?1:(l.e*i-r)/n;o.push([u,d])}return o.length||o.push([0,0]),o},vt.prototype.releasePathsData=function(e){var t,n=e.length;for(t=0;t1?1+r:this.s.v<0?0+r:this.s.v+r,n=this.e.v>1?1+r:this.e.v<0?0+r:this.e.v+r,t>n){var i=t;t=n,n=i}t=Math.round(t*1e4)*1e-4,n=Math.round(n*1e4)*1e-4,this.sValue=t,this.eValue=n}else t=this.sValue,n=this.eValue;var a,o,s=this.shapes.length,c,l,u,d,f,p=0;if(n===t)for(o=0;o=0;--o)if(h=this.shapes[o],h.shape._mdf){for(g=h.localShapeCollection,g.releaseShapes(),this.m===2&&s>1?(b=this.calculateShapeEdges(t,n,h.totalShapeLength,y,p),y+=h.totalShapeLength):b=[[_,v]],l=b.length,c=0;c=1?m.push({s:h.totalShapeLength*(_-1),e:h.totalShapeLength*(v-1)}):(m.push({s:h.totalShapeLength*_,e:h.totalShapeLength}),m.push({s:0,e:h.totalShapeLength*(v-1)}));var x=this.addShapes(h,m[0]);if(m[0].s!==m[0].e){if(m.length>1)if(h.shape.paths.shapes[h.shape.paths._length-1].c){var S=x.pop();this.addPaths(x,g),x=this.addShapes(h,m[1],S)}else this.addPaths(x,g),x=this.addShapes(h,m[1]);this.addPaths(x,g)}}h.shape.paths=g}}else if(this._mdf)for(o=0;ot.e){n.c=!1;break}else t.s<=l&&t.e>=l+u.addedLength?(this.addSegment(i[a].v[s-1],i[a].o[s-1],i[a].i[s],i[a].v[s],n,d,g),g=!1):(p=Ae.getNewSegment(i[a].v[s-1],i[a].v[s],i[a].o[s-1],i[a].i[s],(t.s-l)/u.addedLength,(t.e-l)/u.addedLength,f[s-1]),this.addSegmentFromArray(p,n,d,g),g=!1,n.c=!1),l+=u.addedLength,d+=1;if(i[a].c&&f.length){if(u=f[s-1],l<=t.e){var _=f[s-1].addedLength;t.s<=l&&t.e>=l+_?(this.addSegment(i[a].v[s-1],i[a].o[s-1],i[a].i[0],i[a].v[0],n,d,g),g=!1):(p=Ae.getNewSegment(i[a].v[s-1],i[a].v[0],i[a].o[s-1],i[a].i[0],(t.s-l)/_,(t.e-l)/_,f[s-1]),this.addSegmentFromArray(p,n,d,g),g=!1,n.c=!1)}else n.c=!1;l+=u.addedLength,d+=1}if(n._length&&(n.setXYAt(n.v[h][0],n.v[h][1],`i`,h),n.setXYAt(n.v[n._length-1][0],n.v[n._length-1][1],`o`,n._length-1)),l>t.e)break;a=this.p.keyframes[this.p.keyframes.length-1].t?(r=this.p.getValueAtTime(this.p.keyframes[this.p.keyframes.length-1].t/n,0),i=this.p.getValueAtTime((this.p.keyframes[this.p.keyframes.length-1].t-.05)/n,0)):(r=this.p.pv,i=this.p.getValueAtTime((this.p._caching.lastFrame+this.p.offsetTime-.01)/n,this.p.offsetTime));else if(this.px&&this.px.keyframes&&this.py.keyframes&&this.px.getValueAtTime&&this.py.getValueAtTime){r=[],i=[];var a=this.px,o=this.py;a._caching.lastFrame+a.offsetTime<=a.keyframes[0].t?(r[0]=a.getValueAtTime((a.keyframes[0].t+.01)/n,0),r[1]=o.getValueAtTime((o.keyframes[0].t+.01)/n,0),i[0]=a.getValueAtTime(a.keyframes[0].t/n,0),i[1]=o.getValueAtTime(o.keyframes[0].t/n,0)):a._caching.lastFrame+a.offsetTime>=a.keyframes[a.keyframes.length-1].t?(r[0]=a.getValueAtTime(a.keyframes[a.keyframes.length-1].t/n,0),r[1]=o.getValueAtTime(o.keyframes[o.keyframes.length-1].t/n,0),i[0]=a.getValueAtTime((a.keyframes[a.keyframes.length-1].t-.01)/n,0),i[1]=o.getValueAtTime((o.keyframes[o.keyframes.length-1].t-.01)/n,0)):(r=[a.pv,o.pv],i[0]=a.getValueAtTime((a._caching.lastFrame+a.offsetTime-.01)/n,a.offsetTime),i[1]=o.getValueAtTime((o._caching.lastFrame+o.offsetTime-.01)/n,o.offsetTime))}else i=e,r=i;this.v.rotate(-Math.atan2(r[1]-i[1],r[0]-i[0]))}this.data.p&&this.data.p.s?this.data.p.z?this.v.translate(this.px.v,this.py.v,-this.pz.v):this.v.translate(this.px.v,this.py.v,0):this.v.translate(this.p.v[0],this.p.v[1],-this.p.v[2])}this.frameId=this.elem.globalData.frameId}}function r(){if(this.appliedTransformations=0,this.pre.reset(),!this.a.effectsSequence.length)this.pre.translate(-this.a.v[0],-this.a.v[1],this.a.v[2]),this.appliedTransformations=1;else return;if(!this.s.effectsSequence.length)this.pre.scale(this.s.v[0],this.s.v[1],this.s.v[2]),this.appliedTransformations=2;else return;if(this.sk)if(!this.sk.effectsSequence.length&&!this.sa.effectsSequence.length)this.pre.skewFromAxis(-this.sk.v,this.sa.v),this.appliedTransformations=3;else return;this.r?this.r.effectsSequence.length||(this.pre.rotate(-this.r.v),this.appliedTransformations=4):!this.rz.effectsSequence.length&&!this.ry.effectsSequence.length&&!this.rx.effectsSequence.length&&!this.or.effectsSequence.length&&(this.pre.rotateZ(-this.rz.v).rotateY(this.ry.v).rotateX(this.rx.v).rotateZ(-this.or.v[2]).rotateY(this.or.v[1]).rotateX(this.or.v[0]),this.appliedTransformations=4)}function i(){}function a(e){this._addDynamicProperty(e),this.elem.addDynamicProperty(e),this._isDirty=!0}function o(e,t,n){if(this.elem=e,this.frameId=-1,this.propType=`transform`,this.data=t,this.v=new Ze,this.pre=new Ze,this.appliedTransformations=0,this.initDynamicPropertyContainer(n||e),t.p&&t.p.s?(this.px=B.getProp(e,t.p.x,0,0,this),this.py=B.getProp(e,t.p.y,0,0,this),t.p.z&&(this.pz=B.getProp(e,t.p.z,0,0,this))):this.p=B.getProp(e,t.p||{k:[0,0,0]},1,0,this),t.rx){if(this.rx=B.getProp(e,t.rx,0,D,this),this.ry=B.getProp(e,t.ry,0,D,this),this.rz=B.getProp(e,t.rz,0,D,this),t.or.k[0].ti){var r,i=t.or.k.length;for(r=0;r0;)--n,this._elements.unshift(t[n]);this.dynamicProperties.length?this.k=!0:this.getValue(!0)},xt.prototype.resetElements=function(e){var t,n=e.length;for(t=0;t0?Math.floor(f):Math.ceil(f),h=this.pMatrix.props,g=this.rMatrix.props,_=this.sMatrix.props;this.pMatrix.reset(),this.rMatrix.reset(),this.sMatrix.reset(),this.tMatrix.reset(),this.matrix.reset();var v=0;if(f>0){for(;vm;)this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!0),--v;p&&(this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,-p,!0),v-=p)}r=this.data.m===1?0:this._currentCopies-1,i=this.data.m===1?1:-1,a=this._currentCopies;for(var y,b;a;){if(t=this.elemsData[r].it,n=t[t.length-1].transform.mProps.v.props,b=n.length,t[t.length-1].transform.mProps._mdf=!0,t[t.length-1].transform.op._mdf=!0,t[t.length-1].transform.op.v=this._currentCopies===1?this.so.v:this.so.v+(this.eo.v-this.so.v)*(r/(this._currentCopies-1)),v!==0){for((r!==0&&i===1||r!==this._currentCopies-1&&i===-1)&&this.applyTransforms(this.pMatrix,this.rMatrix,this.sMatrix,this.tr,1,!1),this.matrix.transform(g[0],g[1],g[2],g[3],g[4],g[5],g[6],g[7],g[8],g[9],g[10],g[11],g[12],g[13],g[14],g[15]),this.matrix.transform(_[0],_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13],_[14],_[15]),this.matrix.transform(h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7],h[8],h[9],h[10],h[11],h[12],h[13],h[14],h[15]),y=0;y0&&r<1?[t]:[]:[t-r,t+r].filter(function(e){return e>0&&e<1})},kt.prototype.split=function(e){if(e<=0)return[Ot(this.points[0]),this];if(e>=1)return[this,Ot(this.points[this.points.length-1])];var t=Et(this.points[0],this.points[1],e),n=Et(this.points[1],this.points[2],e),r=Et(this.points[2],this.points[3],e),i=Et(t,n,e),a=Et(n,r,e),o=Et(i,a,e);return[new kt(this.points[0],t,i,o,!0),new kt(o,a,r,this.points[3],!0)]};function G(e,t){var n=e.points[0][t],r=e.points[e.points.length-1][t];if(n>r){var i=r;r=n,n=i}for(var a=W(3*e.a[t],2*e.b[t],e.c[t]),o=0;o0&&a[o]<1){var s=e.point(a[o])[t];sr&&(r=s)}return{min:n,max:r}}kt.prototype.bounds=function(){return{x:G(this,0),y:G(this,1)}},kt.prototype.boundingBox=function(){var e=this.bounds();return{left:e.x.min,right:e.x.max,top:e.y.min,bottom:e.y.max,width:e.x.max-e.x.min,height:e.y.max-e.y.min,cx:(e.x.max+e.x.min)/2,cy:(e.y.max+e.y.min)/2}};function K(e,t,n){var r=e.boundingBox();return{cx:r.cx,cy:r.cy,width:r.width,height:r.height,bez:e,t:(t+n)/2,t1:t,t2:n}}function q(e){var t=e.bez.split(.5);return[K(t[0],e.t1,e.t),K(t[1],e.t,e.t2)]}function J(e,t){return Math.abs(e.cx-t.cx)*2=a||e.width<=r&&e.height<=r&&t.width<=r&&t.height<=r){i.push([e.t,t.t]);return}var o=q(e),s=q(t);Y(o[0],s[0],n+1,r,i,a),Y(o[0],s[1],n+1,r,i,a),Y(o[1],s[0],n+1,r,i,a),Y(o[1],s[1],n+1,r,i,a)}}kt.prototype.intersections=function(e,t,n){t===void 0&&(t=2),n===void 0&&(n=7);var r=[];return Y(K(this,0,1),K(e,0,1),0,t,r,n),r},kt.shapeSegment=function(e,t){var n=(t+1)%e.length();return new kt(e.v[t],e.o[t],e.i[n],e.v[n],!0)},kt.shapeSegmentInverted=function(e,t){var n=(t+1)%e.length();return new kt(e.v[n],e.i[n],e.o[t],e.v[t],!0)};function At(e,t){return[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]]}function jt(e,t,n,r){var i=[e[0],e[1],1],a=[t[0],t[1],1],o=[n[0],n[1],1],s=[r[0],r[1],1],c=At(At(i,a),At(o,s));return wt(c[2])?null:[c[0]/c[2],c[1]/c[2]]}function X(e,t,n){return[e[0]+Math.cos(t)*n,e[1]-Math.sin(t)*n]}function Mt(e,t){return Math.hypot(e[0]-t[0],e[1]-t[1])}function Nt(e,t){return Ct(e[0],t[0])&&Ct(e[1],t[1])}function Pt(){}u([_t],Pt),Pt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amplitude=B.getProp(e,t.s,0,null,this),this.frequency=B.getProp(e,t.r,0,null,this),this.pointsType=B.getProp(e,t.pt,0,null,this),this._isAnimated=this.amplitude.effectsSequence.length!==0||this.frequency.effectsSequence.length!==0||this.pointsType.effectsSequence.length!==0};function Ft(e,t,n,r,i,a,o){var s=n-Math.PI/2,c=n+Math.PI/2,l=t[0]+Math.cos(n)*r*i,u=t[1]-Math.sin(n)*r*i;e.setTripleAt(l,u,l+Math.cos(s)*a,u-Math.sin(s)*a,l+Math.cos(c)*o,u-Math.sin(c)*o,e.length())}function It(e,t){var n=[t[0]-e[0],t[1]-e[1]],r=-Math.PI*.5;return[Math.cos(r)*n[0]-Math.sin(r)*n[1],Math.sin(r)*n[0]+Math.cos(r)*n[1]]}function Lt(e,t){var n=t===0?e.length()-1:t-1,r=(t+1)%e.length(),i=e.v[n],a=e.v[r],o=It(i,a);return Math.atan2(0,1)-Math.atan2(o[1],o[0])}function Rt(e,t,n,r,i,a,o){var s=Lt(t,n),c=t.v[n%t._length],l=t.v[n===0?t._length-1:n-1],u=t.v[(n+1)%t._length],d=a===2?Math.sqrt((c[0]-l[0])**2+(c[1]-l[1])**2):0,f=a===2?Math.sqrt((c[0]-u[0])**2+(c[1]-u[1])**2):0;Ft(e,t.v[n%t._length],s,o,r,f/((i+1)*2),d/((i+1)*2),a)}function zt(e,t,n,r,i,a){for(var o=0;o1&&t.length>1&&(i=Ut(e[0],t[t.length-1]),i)?[[e[0].split(i[0])[0]],[t[t.length-1].split(i[1])[1]]]:[n,r]}function Gt(e){for(var t,n=1;n1&&(t=Wt(e[e.length-1],e[0]),e[e.length-1]=t[0],e[0]=t[1]),e}function Kt(e,t){var n=e.inflectionPoints(),r,i,a,o;if(n.length===0)return[Vt(e,t)];if(n.length===1||Ct(n[1],1))return a=e.split(n[0]),r=a[0],i=a[1],[Vt(r,t),Vt(i,t)];a=e.split(n[0]),r=a[0];var s=(n[1]-n[0])/(1-n[0]);return a=a[1].split(s),o=a[0],i=a[1],[Vt(r,t),Vt(o,t),Vt(i,t)]}function qt(){}u([_t],qt),qt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amount=B.getProp(e,t.a,0,null,this),this.miterLimit=B.getProp(e,t.ml,0,null,this),this.lineJoin=t.lj,this._isAnimated=this.amount.effectsSequence.length!==0},qt.prototype.processPath=function(e,t,n,r){var i=V.newElement();i.c=e.c;var a=e.length();e.c||--a;var o,s,c,l=[];for(o=0;o=0;--o)c=kt.shapeSegmentInverted(e,o),l.push(Kt(c,t));l=Gt(l);var u=null,d=null;for(o=0;o0&&(o=!1),o){var u=l(`style`);u.setAttribute(`f-forigin`,n[r].fOrigin),u.setAttribute(`f-origin`,n[r].origin),u.setAttribute(`f-family`,n[r].fFamily),u.type=`text/css`,u.innerText=`@font-face {font-family: `+n[r].fFamily+`; font-style: normal; src: url('`+n[r].fPath+`');}`,t.appendChild(u)}}else if(n[r].fOrigin===`g`||n[r].origin===1){for(s=document.querySelectorAll(`link[f-forigin="g"], link[f-origin="1"]`),c=0;c=55296&&n<=56319){var r=e.charCodeAt(1);r>=56320&&r<=57343&&(t=(n-55296)*1024+r-56320+65536)}return t}function S(e,t){var n=e.toString(16)+t.toString(16);return d.indexOf(n)!==-1}function C(e){return e===s}function w(e){return e===o}function T(e){var t=x(e);return t>=c&&t<=u}function E(e){return T(e.substr(0,2))&&T(e.substr(2,2))}function D(e){return t.indexOf(e)!==-1}function O(e,t){var o=x(e.substr(t,2));if(o!==n)return!1;var s=0;for(t+=2;s<5;){if(o=x(e.substr(t,2)),oa)return!1;s+=1,t+=2}return x(e.substr(t,2))===r}function k(){this.isLoaded=!0}var A=function(){this.fonts=[],this.chars=null,this.typekitLoaded=0,this.isLoaded=!1,this._warned=!1,this.initTime=Date.now(),this.setIsLoadedBinded=this.setIsLoaded.bind(this),this.checkLoadedFontsBinded=this.checkLoadedFonts.bind(this)};return A.isModifier=S,A.isZeroWidthJoiner=C,A.isFlagEmoji=E,A.isRegionalCode=T,A.isCombinedCharacter=D,A.isRegionalFlag=O,A.isVariationSelector=w,A.BLACK_FLAG_CODE_POINT=n,A.prototype={addChars:_,addFonts:g,getCharData:v,getFontByName:b,measureText:y,checkLoadedFonts:m,setIsLoaded:k},A}();function Xt(e){this.animationData=e}Xt.prototype.getProp=function(e){return this.animationData.slots&&this.animationData.slots[e.sid]?Object.assign(e,this.animationData.slots[e.sid].p):e};function Zt(e){return new Xt(e)}function Qt(){}Qt.prototype={initRenderable:function(){this.isInRange=!1,this.hidden=!1,this.isTransparent=!1,this.renderableComponents=[]},addRenderableComponent:function(e){this.renderableComponents.indexOf(e)===-1&&this.renderableComponents.push(e)},removeRenderableComponent:function(e){this.renderableComponents.indexOf(e)!==-1&&this.renderableComponents.splice(this.renderableComponents.indexOf(e),1)},prepareRenderableFrame:function(e){this.checkLayerLimits(e)},checkTransparency:function(){this.finalTransform.mProp.o.v<=0?!this.isTransparent&&this.globalData.renderConfig.hideOnTransparent&&(this.isTransparent=!0,this.hide()):this.isTransparent&&(this.isTransparent=!1,this.show())},checkLayerLimits:function(e){this.data.ip-this.data.st<=e&&this.data.op-this.data.st>e?this.isInRange!==!0&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):this.isInRange!==!1&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){var e,t=this.renderableComponents.length;for(e=0;e.1)&&this.audio.seek(this._currentTime/this.globalData.frameRate):(this.audio.play(),this.audio.seek(this._currentTime/this.globalData.frameRate),this._isPlaying=!0))},pn.prototype.show=function(){},pn.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},pn.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},pn.prototype.resume=function(){this._canPlay=!0},pn.prototype.setRate=function(e){this.audio.rate(e)},pn.prototype.volume=function(e){this._volumeMultiplier=e,this._previousVolume=e*this._volume,this.audio.volume(this._previousVolume)},pn.prototype.getBaseElement=function(){return null},pn.prototype.destroy=function(){},pn.prototype.sourceRectAtTime=function(){},pn.prototype.initExpressions=function(){};function mn(){}mn.prototype.checkLayers=function(e){var t,n=this.layers.length,r;for(this.completeLayers=!0,t=n-1;t>=0;--t)this.elements[t]||(r=this.layers[t],r.ip-r.st<=e-this.layers[t].st&&r.op-r.st>e-this.layers[t].st&&this.buildItem(t)),this.completeLayers=this.elements[t]?this.completeLayers:!1;this.checkPendingElements()},mn.prototype.createItem=function(e){switch(e.ty){case 2:return this.createImage(e);case 0:return this.createComp(e);case 1:return this.createSolid(e);case 3:return this.createNull(e);case 4:return this.createShape(e);case 5:return this.createText(e);case 6:return this.createAudio(e);case 13:return this.createCamera(e);case 15:return this.createFootage(e);default:return this.createNull(e)}},mn.prototype.createCamera=function(){throw Error(`You're using a 3d camera. Try the html renderer.`)},mn.prototype.createAudio=function(e){return new pn(e,this.globalData,this)},mn.prototype.createFootage=function(e){return new fn(e,this.globalData,this)},mn.prototype.buildAllItems=function(){var e,t=this.layers.length;for(e=0;e0&&(this.maskElement.setAttribute(`id`,p),this.element.maskedElement.setAttribute(b,`url(`+c()+`#`+p+`)`),r.appendChild(this.maskElement)),this.viewData.length&&this.element.addRenderableComponent(this)}_n.prototype.getMaskProperty=function(e){return this.viewData[e].prop},_n.prototype.renderFrame=function(e){var t=this.element.finalTransform.mat,n,r=this.masksProperties.length;for(n=0;n1&&(r+=` C`+t.o[i-1][0]+`,`+t.o[i-1][1]+` `+t.i[0][0]+`,`+t.i[0][1]+` `+t.v[0][0]+`,`+t.v[0][1]),n.lastPath!==r){var o=``;n.elem&&(t.c&&(o=e.inv?this.solidPath+r:r),n.elem.setAttribute(`d`,o)),n.lastPath=r}},_n.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var vn=function(){var e={};e.createFilter=t,e.createAlphaToLuminanceFilter=n;function t(e,t){var n=R(`filter`);return n.setAttribute(`id`,e),t!==!0&&(n.setAttribute(`filterUnits`,`objectBoundingBox`),n.setAttribute(`x`,`0%`),n.setAttribute(`y`,`0%`),n.setAttribute(`width`,`100%`),n.setAttribute(`height`,`100%`)),n}function n(){var e=R(`feColorMatrix`);return e.setAttribute(`type`,`matrix`),e.setAttribute(`color-interpolation-filters`,`sRGB`),e.setAttribute(`values`,`0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 1`),e}return e}(),yn=function(){var e={maskType:!0,svgLumaHidden:!0,offscreenCanvas:typeof OffscreenCanvas<`u`};return(/MSIE 10/i.test(navigator.userAgent)||/MSIE 9/i.test(navigator.userAgent)||/rv:11.0/i.test(navigator.userAgent)||/Edge\/\d./i.test(navigator.userAgent))&&(e.maskType=!1),/firefox/i.test(navigator.userAgent)&&(e.svgLumaHidden=!1),e}(),bn={},xn=`filter_result_`;function Sn(e){var t,n=`SourceGraphic`,r=e.data.ef?e.data.ef.length:0,i=ee(),a=vn.createFilter(i,!0),o=0;this.filters=[];var s;for(t=0;t=0&&(n=this.shapeModifiers[e].processShapes(this._isFirstFrame),!n);--e);}},searchProcessedElement:function(e){for(var t=this.processedElements,n=0,r=t.length;n.01)return!1;n+=1}return!0},Ln.prototype.checkCollapsable=function(){if(this.o.length/2!=this.c.length/4)return!1;if(this.data.k.k[0].s)for(var e=0,t=this.data.k.k.length;e0;)c=r.transformers[g].mProps._mdf||c,--h,--g;if(c)for(h=f-r.styles[u].lvl,g=r.transformers.length-1;h>0;)m.multiply(r.transformers[g].mProps.v),--h,--g}else m=e;if(p=r.sh.paths,o=p._length,c){for(s=``,a=0;a=1?v=.99:v<=-1&&(v=-.99);var y=g*v,b=Math.cos(_+t.a.v)*y+a[0],x=Math.sin(_+t.a.v)*y+a[1];r.setAttribute(`fx`,b),r.setAttribute(`fy`,x),i&&!t.g._collapsable&&(t.of.setAttribute(`fx`,b),t.of.setAttribute(`fy`,x))}}}function u(e,t,n){var r=t.style,i=t.d;i&&(i._mdf||n)&&i.dashStr&&(r.pElem.setAttribute(`stroke-dasharray`,i.dashStr),r.pElem.setAttribute(`stroke-dashoffset`,i.dashoffset[0])),t.c&&(t.c._mdf||n)&&r.pElem.setAttribute(`stroke`,`rgb(`+C(t.c.v[0])+`,`+C(t.c.v[1])+`,`+C(t.c.v[2])+`)`),(t.o._mdf||n)&&r.pElem.setAttribute(`stroke-opacity`,t.o.v),(t.w._mdf||n)&&(r.pElem.setAttribute(`stroke-width`,t.w.v),r.msElem&&r.msElem.setAttribute(`stroke-width`,t.w.v))}return n}();function Wn(e,t,n){this.shapes=[],this.shapesData=e.shapes,this.stylesList=[],this.shapeModifiers=[],this.itemsData=[],this.processedElements=[],this.animatedContents=[],this.initElement(e,t,n),this.prevViewData=[]}u([un,gn,Cn,On,wn,dn,Tn],Wn),Wn.prototype.initSecondaryElement=function(){},Wn.prototype.identityMatrix=new Ze,Wn.prototype.buildExpressionInterface=function(){},Wn.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},Wn.prototype.filterUniqueShapes=function(){var e,t=this.shapes.length,n,r,i=this.stylesList.length,a,o=[],s=!1;for(r=0;r1&&s&&this.setShapesAsAnimated(o)}},Wn.prototype.setShapesAsAnimated=function(e){var t,n=e.length;for(t=0;t=0;--c){if(g=this.searchProcessedElement(e[c]),g?t[c]=n[g-1]:e[c]._render=o,e[c].ty===`fl`||e[c].ty===`st`||e[c].ty===`gf`||e[c].ty===`gs`||e[c].ty===`no`)g?t[c].style.closed=e[c].hd:t[c]=this.createStyleElement(e[c],i),e[c]._render&&t[c].style.pElem.parentNode!==r&&r.appendChild(t[c].style.pElem),f.push(t[c].style);else if(e[c].ty===`gr`){if(!g)t[c]=this.createGroupElement(e[c]);else for(d=t[c].it.length,u=0;u1,this.kf&&this.addEffect(this.getKeyframeValue.bind(this)),this.kf},Kn.prototype.addEffect=function(e){this.effectsSequence.push(e),this.elem.addDynamicProperty(this)},Kn.prototype.getValue=function(e){if(!((this.elem.globalData.frameId===this.frameId||!this.effectsSequence.length)&&!e)){this.currentData.t=this.data.d.k[this.keysIndex].s.t;var t=this.currentData,n=this.keysIndex;if(this.lock){this.setCurrentData(this.currentData);return}this.lock=!0,this._mdf=!1;var r,i=this.effectsSequence.length,a=e||this.data.d.k[this.keysIndex].s;for(r=0;rt);)n+=1;return this.keysIndex!==n&&(this.keysIndex=n),this.data.d.k[this.keysIndex].s},Kn.prototype.buildFinalText=function(e){for(var t=[],n=0,r=e.length,i,a,o=!1,s=!1,c=``;n=55296&&i<=56319?Yt.isRegionalFlag(e,n)?c=e.substr(n,14):(a=e.charCodeAt(n+1),a>=56320&&a<=57343&&(Yt.isModifier(i,a)?(c=e.substr(n,2),o=!0):c=Yt.isFlagEmoji(e.substr(n,4))?e.substr(n,4):e.substr(n,2))):i>56319?(a=e.charCodeAt(n+1),Yt.isVariationSelector(i)&&(o=!0)):Yt.isZeroWidthJoiner(i)&&(o=!0,s=!0),o?(t[t.length-1]+=c,o=!1):t.push(c),n+=c.length;return t},Kn.prototype.completeTextData=function(e){e.__complete=!0;var t=this.elem.globalData.fontManager,n=this.data,r=[],i,a,o,s=0,c,l=n.m.g,u=0,d=0,f=0,p=[],m=0,h=0,g,_,v=t.getFontByName(e.f),y,b=0,x=Jt(v);e.fWeight=x.weight,e.fStyle=x.style,e.finalSize=e.s,e.finalText=this.buildFinalText(e.t),a=e.finalText.length,e.finalLineHeight=e.lh;var S=e.tr/1e3*e.finalSize,C;if(e.sz)for(var w=!0,T=e.sz[0],E=e.sz[1],D,O;w;){O=this.buildFinalText(e.t),D=0,m=0,a=O.length,S=e.tr/1e3*e.finalSize;var k=-1;for(i=0;iT&&O[i]!==` `?(k===-1?a+=1:i=k,D+=e.finalLineHeight||e.finalSize*1.2,O.splice(i,+(k===i),`\r`),k=-1,m=0):(m+=b,m+=S);D+=v.ascent*e.finalSize/100,this.canResize&&e.finalSize>this.minimumFontSize&&Eh?m:h,m=-2*S,c=``,o=!0,f+=1):c=j,t.chars?(y=t.getCharData(j,v.fStyle,t.getFontByName(e.f).fFamily),b=o?0:y.w*e.finalSize/100):b=t.measureText(c,e.f,e.finalSize),j===` `?A+=b+S:(m+=b+S+A,A=0),r.push({l:b,an:b,add:u,n:o,anIndexes:[],val:c,line:f,animatorJustifyOffset:0}),l==2){if(u+=b,c===``||c===` `||i===a-1){for((c===``||c===` `)&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;s+=1,u=0}}else if(l==3){if(u+=b,c===``||i===a-1){for(c===``&&(u-=b);d<=i;)r[d].an=u,r[d].ind=s,r[d].extra=b,d+=1;u=0,s+=1}}else r[s].ind=s,r[s].extra=0,s+=1;if(e.l=r,h=m>h?m:h,p.push(m),e.sz)e.boxWidth=e.sz[0],e.justifyOffset=0;else switch(e.boxWidth=h,e.j){case 1:e.justifyOffset=-e.boxWidth;break;case 2:e.justifyOffset=-e.boxWidth/2;break;default:e.justifyOffset=0}e.lineWidths=p;var M=n.a,N,P;_=M.length;var F,ee,I=[];for(g=0;g<_;g+=1){for(N=M[g],N.a.sc&&(e.strokeColorAnim=!0),N.a.sw&&(e.strokeWidthAnim=!0),(N.a.fc||N.a.fh||N.a.fs||N.a.fb)&&(e.fillColorAnim=!0),ee=0,F=N.s.b,i=0;i0?i=this.ne.v/100:a=-this.ne.v/100,this.xe.v>0?o=1-this.xe.v/100:s=1+this.xe.v/100;var c=we.getBezierEasing(i,a,o,s).get,l=0,u=this.finalS,d=this.finalE,f=this.data.sh;if(f===2)l=d===u?+(r>=d):e(0,t(.5/(d-u)+(r-u)/(d-u),1)),l=c(l);else if(f===3)l=d===u?r>=d?0:1:1-e(0,t(.5/(d-u)+(r-u)/(d-u),1)),l=c(l);else if(f===4)d===u?l=0:(l=e(0,t(.5/(d-u)+(r-u)/(d-u),1)),l<.5?l*=2:l=1-2*(l-.5)),l=c(l);else if(f===5){if(d===u)l=0;else{var p=d-u;r=t(e(0,r+.5-u),d-u);var m=-p/2+r,h=p/2;l=Math.sqrt(1-m*m/(h*h))}l=c(l)}else f===6?(d===u?l=0:(r=t(e(0,r+.5-u),d-u),l=(1+Math.cos(Math.PI+Math.PI*2*r/(d-u)))/2),l=c(l)):(r>=n(u)&&(l=r-u<0?e(0,t(t(d,1)-(u-r),1)):e(0,t(d-r,1))),l=c(l));if(this.sm.v!==100){var g=this.sm.v*.01;g===0&&(g=1e-8);var _=.5-g*.5;l<_?l=0:(l=(l-_)/g,l>1&&(l=1))}return l*this.a.v},getValue:function(e){this.iterateDynamicProperties(),this._mdf=e||this._mdf,this._currentTextLength=this.elem.textProperty.currentData.l.length||0,e&&this.data.r===2&&(this.e.v=this._currentTextLength);var t=this.data.r===2?1:100/this.data.totalChars,n=this.o.v/t,r=this.s.v/t+n,i=this.e.v/t+n;if(r>i){var a=r;r=i,i=a}this.finalS=r,this.finalE=i}},u([Ge],r);function i(e,t,n){return new r(e,t,n)}return{getTextSelectorProp:i}}();function Jn(e,t,n){var r={propType:!1},i=B.getProp,a=t.a;this.a={r:a.r?i(e,a.r,0,D,n):r,rx:a.rx?i(e,a.rx,0,D,n):r,ry:a.ry?i(e,a.ry,0,D,n):r,sk:a.sk?i(e,a.sk,0,D,n):r,sa:a.sa?i(e,a.sa,0,D,n):r,s:a.s?i(e,a.s,1,.01,n):r,a:a.a?i(e,a.a,1,0,n):r,o:a.o?i(e,a.o,0,.01,n):r,p:a.p?i(e,a.p,1,0,n):r,sw:a.sw?i(e,a.sw,0,0,n):r,sc:a.sc?i(e,a.sc,1,0,n):r,fc:a.fc?i(e,a.fc,1,0,n):r,fh:a.fh?i(e,a.fh,0,0,n):r,fs:a.fs?i(e,a.fs,0,.01,n):r,fb:a.fb?i(e,a.fb,0,.01,n):r,t:a.t?i(e,a.t,0,0,n):r},this.s=qn.getTextSelectorProp(e,t.s,n),this.s.t=t.s.t}function Yn(e,t,n){this._isFirstFrame=!0,this._hasMaskedPath=!1,this._frameId=-1,this._textData=e,this._renderType=t,this._elem=n,this._animatorsData=m(this._textData.a.length),this._pathData={},this._moreOptions={alignment:{}},this.renderedLetters=[],this.lettersChangedFlag=!1,this.initDynamicPropertyContainer(n)}Yn.prototype.searchProperties=function(){var e,t=this._textData.a.length,n,r=B.getProp;for(e=0;e=m+Te||!x?(T=(m+Te-g)/h.partialLength,ae=b.point[0]+(h.point[0]-b.point[0])*T,oe=b.point[1]+(h.point[1]-b.point[1])*T,a.translate(-n[0]*f[u].an*.005,-(n[1]*A)*.01),_=!1):x&&(g+=h.partialLength,v+=1,v>=x.length&&(v=0,y+=1,S[y]?x=S[y].points:D.v.c?(v=0,y=0,x=S[y].points):(g-=h.partialLength,x=null)),x&&(b=h,h=x[v],C=h.partialLength));ie=f[u].an/2-f[u].add,a.translate(-ie,0,0)}else ie=f[u].an/2-f[u].add,a.translate(-ie,0,0),a.translate(-n[0]*f[u].an*.005,-n[1]*A*.01,0);for(P=0;Pe?this.textSpans[e].span:R(s?`g`:`text`),b<=e){if(c.setAttribute(`stroke-linecap`,`butt`),c.setAttribute(`stroke-linejoin`,`round`),c.setAttribute(`stroke-miterlimit`,`4`),this.textSpans[e].span=c,s){var S=R(`g`);c.appendChild(S),this.textSpans[e].childSpan=S}this.textSpans[e].span=c,this.layerElement.appendChild(c)}c.style.display=`inherit`}if(l.reset(),d&&(o[e].n&&(f=-g,p+=n.yOffset,p+=+!!h,h=!1),this.applyTextPropertiesToMatrix(n,l,o[e].line,f,p),f+=o[e].l||0,f+=g),s){x=this.globalData.fontManager.getCharData(n.finalText[e],r.fStyle,this.globalData.fontManager.getFontByName(n.f).fFamily);var C;if(x.t===1)C=new rr(x.data,this.globalData,this);else{var w=Zn;x.data&&x.data.shapes&&(w=this.buildShapeData(x.data,n.finalSize)),C=new Wn(w,this.globalData,this)}if(this.textSpans[e].glyph){var T=this.textSpans[e].glyph;this.textSpans[e].childSpan.removeChild(T.layerElement),T.destroy()}this.textSpans[e].glyph=C,C._debug=!0,C.prepareFrame(0),C.renderFrame(),this.textSpans[e].childSpan.appendChild(C.layerElement),x.t===1&&this.textSpans[e].childSpan.setAttribute(`transform`,`scale(`+n.finalSize/100+`,`+n.finalSize/100+`)`)}else d&&c.setAttribute(`transform`,`translate(`+l.props[12]+`,`+l.props[13]+`)`),c.textContent=o[e].val,c.setAttributeNS(`http://www.w3.org/XML/1998/namespace`,`xml:space`,`preserve`)}d&&c&&c.setAttribute(`d`,u)}for(;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].prepareFrame(e-this.layers[t].st);if(this.globalData._mdf)for(t=0;t=0;--n)(this.completeLayers||this.elements[n])&&(this.elements[n].prepareFrame(this.renderedFrame-this.layers[n].st),this.elements[n]._mdf&&(this._mdf=!0))}},nr.prototype.renderInnerContent=function(){var e,t=this.layers.length;for(e=0;e=0;--n)e.finalTransform.multiply(e.transforms[n].transform.mProps.v);e._mdf=i},processSequences:function(e){var t,n=this.sequenceList.length;for(t=0;t=1){this.buffers=[];var e=this.globalData.canvasContext,t=cr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(t);var n=cr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(n),this.data.tt>=3&&!document._isProxy&&cr.loadLumaCanvas()}this.canvasContext=this.globalData.canvasContext,this.transformCanvas=this.globalData.transformCanvas,this.renderableEffectsManager=new ur(this),this.searchEffectTransforms()},createContent:function(){},setBlendMode:function(){var e=this.globalData;if(e.blendMode!==this.data.bm){e.blendMode=this.data.bm;var t=$t(this.data.bm);e.canvasContext.globalCompositeOperation=t}},createRenderableComponents:function(){this.maskManager=new dr(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(hn.TRANSFORM_EFFECT)},hideElement:function(){!this.hidden&&(!this.isInRange||this.isTransparent)&&(this.hidden=!0)},showElement:function(){this.isInRange&&!this.isTransparent&&(this.hidden=!1,this._isFirstFrame=!0,this.maskManager._isFirstFrame=!0)},clearCanvas:function(e){e.clearRect(this.transformCanvas.tx,this.transformCanvas.ty,this.transformCanvas.w*this.transformCanvas.sx,this.transformCanvas.h*this.transformCanvas.sy)},prepareLayer:function(){if(this.data.tt>=1){var e=this.buffers[0].getContext(`2d`);this.clearCanvas(e),e.drawImage(this.canvasContext.canvas,0,0),this.currentTransform=this.canvasContext.getTransform(),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform)}},exitLayer:function(){if(this.data.tt>=1){var e=this.buffers[1],t=e.getContext(`2d`);if(this.clearCanvas(t),t.drawImage(this.canvasContext.canvas,0,0),this.canvasContext.setTransform(1,0,0,1,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.setTransform(this.currentTransform),this.comp.getElementById(`tp`in this.data?this.data.tp:this.data.ind-1).renderFrame(!0),this.canvasContext.setTransform(1,0,0,1,0,0),this.data.tt>=3&&!document._isProxy){var n=cr.getLumaCanvas(this.canvasContext.canvas);n.getContext(`2d`).drawImage(this.canvasContext.canvas,0,0),this.clearCanvas(this.canvasContext),this.canvasContext.drawImage(n,0,0)}this.canvasContext.globalCompositeOperation=pr[this.data.tt],this.canvasContext.drawImage(e,0,0),this.canvasContext.globalCompositeOperation=`destination-over`,this.canvasContext.drawImage(this.buffers[0],0,0),this.canvasContext.setTransform(this.currentTransform),this.canvasContext.globalCompositeOperation=`source-over`}},renderFrame:function(e){if(!(this.hidden||this.data.hd)&&!(this.data.td===1&&!e)){this.renderTransform(),this.renderRenderable(),this.renderLocalTransform(),this.setBlendMode();var t=this.data.ty===0;this.prepareLayer(),this.globalData.renderer.save(t),this.globalData.renderer.ctxTransform(this.finalTransform.localMat.props),this.globalData.renderer.ctxOpacity(this.finalTransform.localOpacity),this.renderInnerContent(),this.globalData.renderer.restore(t),this.exitLayer(),this.maskManager.hasMasks&&this.globalData.renderer.restore(!0),this._isFirstFrame&&=!1}},destroy:function(){this.canvasContext=null,this.data=null,this.globalData=null,this.maskManager.destroy()},mHelper:new Ze},fr.prototype.hide=fr.prototype.hideElement,fr.prototype.show=fr.prototype.showElement;function mr(e,t,n,r){this.styledShapes=[],this.tr=[0,0,0,0,0,0];var i=4;t.ty===`rc`?i=5:t.ty===`el`?i=6:t.ty===`sr`&&(i=7),this.sh=Xe.getShapeProp(e,t,i,e);var a,o=n.length,s;for(a=0;a=0;--a){if(d=this.searchProcessedElement(e[a]),d?t[a]=n[d-1]:e[a]._shouldRender=r,e[a].ty===`fl`||e[a].ty===`st`||e[a].ty===`gf`||e[a].ty===`gs`)d?t[a].style.closed=!1:t[a]=this.createStyleElement(e[a],m),l.push(t[a].style);else if(e[a].ty===`gr`){if(!d)t[a]=this.createGroupElement(e[a]);else for(c=t[a].it.length,s=0;s=0;--i)t[i].ty===`tr`?(o=n[i].transform,this.renderShapeTransform(e,o)):t[i].ty===`sh`||t[i].ty===`el`||t[i].ty===`rc`||t[i].ty===`sr`?this.renderPath(t[i],n[i]):t[i].ty===`fl`?this.renderFill(t[i],n[i],o):t[i].ty===`st`?this.renderStroke(t[i],n[i],o):t[i].ty===`gf`||t[i].ty===`gs`?this.renderGradientFill(t[i],n[i],o):t[i].ty===`gr`?this.renderShape(o,t[i].it,n[i].it):t[i].ty;r&&this.drawLayer()},hr.prototype.renderStyledShape=function(e,t){if(this._isFirstFrame||t._mdf||e.transforms._mdf){var n=e.trNodes,r=t.paths,i,a,o,s=r._length;n.length=0;var c=e.transforms.finalTransform;for(o=0;o=1?u=.99:u<=-1&&(u=-.99);var d=c*u,f=Math.cos(l+t.a.v)*d+o[0],p=Math.sin(l+t.a.v)*d+o[1];i=a.createRadialGradient(f,p,0,o[0],o[1],c)}var m,h=e.g.p,g=t.g.c,_=1;for(m=0;ma&&c===`xMidYMid slice`||ii&&s===`meet`||ai&&s===`slice`)?this.transformCanvas.tx=(n-this.transformCanvas.w*(r/this.transformCanvas.h))/2*this.renderConfig.dpr:l===`xMax`&&(ai&&s===`slice`)?this.transformCanvas.tx=(n-this.transformCanvas.w*(r/this.transformCanvas.h))*this.renderConfig.dpr:this.transformCanvas.tx=0,u===`YMid`&&(a>i&&s===`meet`||ai&&s===`meet`||a=0;--e)this.elements[e]&&this.elements[e].destroy&&this.elements[e].destroy();this.elements.length=0,this.globalData.canvasContext=null,this.animationItem.container=null,this.destroyed=!0},Q.prototype.renderFrame=function(e,t){if(!(this.renderedFrame===e&&this.renderConfig.clearCanvas===!0&&!t||this.destroyed||e===-1)){this.renderedFrame=e,this.globalData.frameNum=e-this.animationItem._isFirstFrame,this.globalData.frameId+=1,this.globalData._mdf=!this.renderConfig.clearCanvas||t,this.globalData.projectInterface.currentFrame=e;var n,r=this.layers.length;for(this.completeLayers||this.checkLayers(e),n=r-1;n>=0;--n)(this.completeLayers||this.elements[n])&&this.elements[n].prepareFrame(e-this.layers[n].st);if(this.globalData._mdf){for(this.renderConfig.clearCanvas===!0?this.canvasContext.clearRect(0,0,this.transformCanvas.w,this.transformCanvas.h):this.save(),n=r-1;n>=0;--n)(this.completeLayers||this.elements[n])&&this.elements[n].renderFrame();this.renderConfig.clearCanvas!==!0&&this.restore()}}},Q.prototype.buildItem=function(e){var t=this.elements;if(!(t[e]||this.layers[e].ty===99)){var n=this.createItem(this.layers[e],this,this.globalData);t[e]=n,n.initExpressions()}},Q.prototype.checkPendingElements=function(){for(;this.pendingElements.length;)this.pendingElements.pop().checkParenting()},Q.prototype.hide=function(){this.animationItem.container.style.display=`none`},Q.prototype.show=function(){this.animationItem.container.style.display=`block`};function yr(){this.opacity=-1,this.transform=p(`float32`,16),this.fillStyle=``,this.strokeStyle=``,this.lineWidth=``,this.lineCap=``,this.lineJoin=``,this.miterLimit=``,this.id=Math.random()}function br(){this.stack=[],this.cArrPos=0,this.cTr=new Ze;var e,t=15;for(e=0;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].renderFrame()},xr.prototype.destroy=function(){var e;for(e=this.layers.length-1;e>=0;--e)this.elements[e]&&this.elements[e].destroy();this.layers=null,this.elements=null},xr.prototype.createComp=function(e){return new xr(e,this.globalData,this)};function Sr(e,t){this.animationItem=e,this.renderConfig={clearCanvas:t&&t.clearCanvas!==void 0?t.clearCanvas:!0,context:t&&t.context||null,progressiveLoad:t&&t.progressiveLoad||!1,preserveAspectRatio:t&&t.preserveAspectRatio||`xMidYMid meet`,imagePreserveAspectRatio:t&&t.imagePreserveAspectRatio||`xMidYMid slice`,contentVisibility:t&&t.contentVisibility||`visible`,className:t&&t.className||``,id:t&&t.id||``,runExpressions:!t||t.runExpressions===void 0||t.runExpressions},this.renderConfig.dpr=t&&t.dpr||1,this.animationItem.wrapper&&(this.renderConfig.dpr=t&&t.dpr||window.devicePixelRatio||1),this.renderedFrame=-1,this.globalData={frameNum:-1,_mdf:!1,renderConfig:this.renderConfig,currentGlobalAlpha:-1},this.contextData=new br,this.elements=[],this.pendingElements=[],this.transformMat=new Ze,this.completeLayers=!1,this.rendererType=`canvas`,this.renderConfig.clearCanvas&&(this.ctxTransform=this.contextData.transform.bind(this.contextData),this.ctxOpacity=this.contextData.opacity.bind(this.contextData),this.ctxFillStyle=this.contextData.fillStyle.bind(this.contextData),this.ctxStrokeStyle=this.contextData.strokeStyle.bind(this.contextData),this.ctxLineWidth=this.contextData.lineWidth.bind(this.contextData),this.ctxLineCap=this.contextData.lineCap.bind(this.contextData),this.ctxLineJoin=this.contextData.lineJoin.bind(this.contextData),this.ctxMiterLimit=this.contextData.miterLimit.bind(this.contextData),this.ctxFill=this.contextData.fill.bind(this.contextData),this.ctxFillRect=this.contextData.fillRect.bind(this.contextData),this.ctxStroke=this.contextData.stroke.bind(this.contextData),this.save=this.contextData.save.bind(this.contextData))}return u([Q],Sr),Sr.prototype.createComp=function(e){return new xr(e,this.globalData,this)},ye(`canvas`,Sr),gt.registerModifier(`tm`,vt),gt.registerModifier(`pb`,yt),gt.registerModifier(`rp`,xt),gt.registerModifier(`rd`,St),gt.registerModifier(`zz`,Pt),gt.registerModifier(`op`,qt),$e}))}))(),1);function fr({documentID:e,className:t=``,showError:n=!0}){let r=(0,g.useRef)(null),i=(0,g.useRef)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(null);return(0,g.useEffect)(()=>{let t=!1,n=null;return o(``),c(null),fetch(k.stickerDocumentAnimationURL(e),{credentials:`same-origin`}).then(async e=>{if(!e.ok){let t=await e.json().catch(()=>null);throw Error(t?.error||e.statusText)}if((e.headers.get(`content-type`)??``).includes(`json`)){let n=await e.json();if(t||!r.current)return;i.current?.destroy(),i.current=dr.default.loadAnimation({container:r.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:n});return}let a=await e.blob();t||(n=URL.createObjectURL(a),c(n))}).catch(e=>{t||o(O(e))}),()=>{t=!0,i.current?.destroy(),i.current=null,n&&URL.revokeObjectURL(n)}},[e]),(0,W.jsxs)(`div`,{className:`sticker-doc-cell ${t}`.trim(),children:[s?(0,W.jsx)(`img`,{className:`sticker-doc-image`,src:s,alt:``}):(0,W.jsx)(`div`,{className:`sticker-doc-canvas`,ref:r}),a&&n&&(0,W.jsx)(`span`,{className:`sticker-doc-error`,children:a})]})}function pr({kind:e,onClose:t,onCreated:n}){let r=e===`emoji`?`emoji`:`sticker`,[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(null),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!1),[_,v]=(0,g.useState)(``);async function y(){if(!i.trim()||!o.trim()||!c.trim()||!u){v(`Title, short name, emoji and a first ${r} file are required.`);return}if(!f.trim()){v(`Please enter an operation reason`);return}h(!0),v(``);try{let r=new FormData;r.set(`metadata`,JSON.stringify({command_id:``,reason:f.trim(),confirm:!0,title:i.trim(),short_name:o.trim().toLowerCase(),kind:e,emoji:c.trim()})),r.set(`file`,u,u.name),await k.createStickerSet(r),n(),t()}catch(e){v(O(e))}finally{h(!1)}}return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Create a new ${r} pack`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`New set`}),(0,W.jsx)(`h2`,{children:`Create a new ${r} pack`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:m,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:`Title`}),(0,W.jsx)(`input`,{value:i,maxLength:64,onChange:e=>a(e.target.value)})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:`Short name`}),(0,W.jsx)(`input`,{value:o,maxLength:32,onChange:e=>s(e.target.value),placeholder:`lowercase_short_name`})]}),(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:`Emoji`}),(0,W.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:`e.g. 😀`})]})]}),(0,W.jsxs)(`label`,{className:`gift-file-picker ${u?`has-file`:``}`,children:[(0,W.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.webp,application/json,application/x-tgsticker,image/webp`,onChange:e=>d(e.target.files?.[0]??null)}),(0,W.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,W.jsx)(`span`,{className:`gift-field-label`,children:`First ${r}`}),(0,W.jsx)(`strong`,{children:u?u.name:`Choose a TGS, Lottie JSON, or WebP file`})]}),(0,W.jsx)(`span`,{className:`gift-file-action`,children:u?`Change file`:`Choose file`})]}),(0,W.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,W.jsx)(`span`,{children:`Audit reason`}),(0,W.jsx)(`input`,{value:f,placeholder:`Briefly describe why this gift is being imported`,onChange:e=>p(e.target.value)})]}),_&&(0,W.jsx)(K,{children:_})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:m,children:`Close`}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:y,disabled:m,children:[m?(0,W.jsx)(L,{className:`spin`,size:15}):(0,W.jsx)(ot,{size:15}),`Create ${r} pack`]})]})]})}),document.body)}var mr=24;function hr({set:e,onClose:t}){let n=e.Kind===`emoji`?`emoji`:`sticker`,[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(1),l=(0,g.useCallback)(()=>{let t=!1;return o(``),k.stickerSetDocuments(e.ID).then(e=>{t||i(e.document_ids??[])}).catch(e=>{t||o(O(e))}),()=>{t=!0}},[e.ID]);(0,g.useEffect)(()=>(i(null),c(1),l()),[l]);let u=r?.length??0,d=Math.max(1,Math.ceil(u/mr)),f=Math.min(s,d),p=(f-1)*mr,m=r?.slice(p,p+mr)??[],h=m.length===0?0:p+1,_=h===0?0:h+m.length-1;return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal sticker-preview-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e.Title||`#${e.ID}`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`Set contents`}),(0,W.jsx)(`h2`,{children:e.Title||`#${e.ID}`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsx)(gr,{setID:e.ID,noun:n,onAdded:l}),a&&(0,W.jsx)(K,{children:a}),!a&&r===null&&(0,W.jsxs)(`div`,{className:`loading-line`,children:[(0,W.jsx)(L,{className:`spin`,size:18}),` `,`Loading`]}),r!==null&&u===0&&!a&&(0,W.jsx)(`div`,{className:`empty-panel`,children:`This set has no documents.`}),m.length>0&&(0,W.jsx)(`div`,{className:`sticker-doc-grid`,children:m.map(t=>(0,W.jsxs)(`div`,{className:`sticker-doc-grid-cell`,children:[(0,W.jsx)(fr,{documentID:t}),(0,W.jsx)(Z,{compact:!0,tone:`danger`,label:`Remove`,icon:(0,W.jsx)(it,{size:12}),path:`/api/actions/remove-sticker-from-set`,payload:()=>({set_id:e.ID,document_id:t}),onDone:l})]},t))},f),u>mr&&(0,W.jsxs)(`div`,{className:`gift-pager`,children:[(0,W.jsx)(`span`,{className:`gift-pager-range`,children:`Showing ${h}-${_} of ${u}`}),(0,W.jsxs)(`div`,{className:`gift-pager-controls`,children:[(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>c(e=>Math.max(1,e-1)),disabled:f<=1,children:[(0,W.jsx)(ge,{size:14}),` `,`Previous`]}),(0,W.jsx)(`span`,{className:`gift-pager-page`,children:`Page ${f} of ${d}`}),(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>c(e=>Math.min(d,e+1)),disabled:f>=d,children:[`Next`,` `,(0,W.jsx)(_e,{size:14})]})]})]})]})]})}),document.body)}function gr({setID:e,noun:t,onAdded:n}){let[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(!1),[d,f]=(0,g.useState)(``);async function p(){if(!r){f(`Choose a ${t} file first`);return}if(!a.trim()){f(`An emoji is required.`);return}if(!s.trim()){f(`Please enter an operation reason`);return}u(!0),f(``);try{let t=new FormData;t.set(`metadata`,JSON.stringify({command_id:``,reason:s.trim(),confirm:!0,set_id:e,emoji:a.trim()})),t.set(`file`,r,r.name),await k.addStickerToSet(t),i(null),o(``),c(``),n()}catch(e){f(O(e))}finally{u(!1)}}return(0,W.jsxs)(`div`,{className:`sticker-add-form`,children:[(0,W.jsxs)(`label`,{className:`gift-file-picker compact ${r?`has-file`:``}`,children:[(0,W.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.webp,application/json,application/x-tgsticker,image/webp`,onChange:e=>i(e.target.files?.[0]??null)}),(0,W.jsx)(`span`,{className:`gift-file-copy`,children:(0,W.jsx)(`strong`,{children:r?r.name:`Choose a TGS, Lottie JSON, or WebP file`})})]}),(0,W.jsx)(`input`,{className:`small-input`,value:a,onChange:e=>o(e.target.value),placeholder:`e.g. 😀`}),(0,W.jsx)(`input`,{className:`small-input`,value:s,onChange:e=>c(e.target.value),placeholder:`Describe why this operation is being performed`}),(0,W.jsxs)(`button`,{className:`btn primary compact-btn`,type:`button`,onClick:p,disabled:l,children:[l?(0,W.jsx)(L,{className:`spin`,size:14}):(0,W.jsx)(Ue,{size:14}),` `,`Add ${t}`]}),d&&(0,W.jsx)(`span`,{className:`sticker-add-form-error`,children:d})]})}function _r({kind:e}){let[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(10),[d,f]=(0,g.useState)(1),[p,m]=(0,g.useState)({}),[h,_]=(0,g.useState)({}),[v,y]=(0,g.useState)(null),[b,x]=(0,g.useState)(!1),S=e===`emoji`?`Emoji`:`Stickers`,C=e===`emoji`?`Custom-emoji packs — system packs aren't shown here, they're not hand-edited`:`Sticker packs — system packs (dice, animated emoji, gifts) aren't shown here, they're not hand-edited`,w=e===`emoji`?`emoji`:`sticker`;async function T(){o(!0),c(``);try{n((await k.stickerSets(e)).rows??[])}catch(e){c(O(e))}finally{o(!1)}}(0,g.useEffect)(()=>{T()},[e]);let E=(0,g.useMemo)(()=>{let e=r.trim().toLowerCase();return e?t.filter(t=>String(t.ID).includes(e)||t.ShortName.toLowerCase().includes(e)||t.Title.toLowerCase().includes(e)):t},[t,r]);(0,g.useEffect)(()=>{f(1)},[r,l,e]);let D=l===`all`?1:Math.max(1,Math.ceil(E.length/l)),A=Math.min(d,D),j=(0,g.useMemo)(()=>{if(l===`all`)return E;let e=(A-1)*l;return E.slice(e,e+l)},[E,A,l]),M=j.length===0?0:l===`all`?1:(A-1)*l+1,N=M===0?0:M+j.length-1,P=(0,g.useMemo)(()=>({total:t.length,official:t.filter(e=>e.Official).length,archived:t.filter(e=>e.Archived).length}),[t]);return(0,W.jsxs)(Dt,{title:S,eyebrow:C,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>T(),disabled:a,children:[(0,W.jsx)(Ke,{size:15}),` `,`Refresh`]}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:()=>x(!0),children:[(0,W.jsx)(Ue,{size:15}),` `,`Create ${w} pack`]})]}),children:[s&&(0,W.jsx)(K,{children:s}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Total sets`,value:String(P.total)}),(0,W.jsx)(J,{label:`Official`,value:String(P.official),tone:`good`}),(0,W.jsx)(J,{label:`Archived`,value:String(P.archived),tone:P.archived>0?`warn`:`neutral`})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`div`,{className:`toolbar`,children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`Search set ID, short name or title`})]}),(0,W.jsxs)(`label`,{className:`gift-page-size`,children:[(0,W.jsx)(`span`,{children:`Per page`}),(0,W.jsxs)(`select`,{value:String(l),onChange:e=>u(e.target.value===`all`?`all`:Number(e.target.value)),children:[(0,W.jsx)(`option`,{value:`10`,children:`10`}),(0,W.jsx)(`option`,{value:`20`,children:`20`}),(0,W.jsx)(`option`,{value:`50`,children:`50`}),(0,W.jsx)(`option`,{value:`100`,children:`100`}),(0,W.jsx)(`option`,{value:`all`,children:`All`})]})]}),(0,W.jsx)(`span`,{className:`gift-list-summary`,children:`Showing ${E.length} of ${t.length}`})]})}),(0,W.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Logo`}),(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Short name`}),(0,W.jsx)(`th`,{children:`Title`}),(0,W.jsx)(`th`,{children:`Documents`}),(0,W.jsx)(`th`,{children:`Official`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Sort order`}),(0,W.jsx)(`th`,{children:`Actions`})]})}),(0,W.jsxs)(`tbody`,{children:[j.map(e=>(0,W.jsxs)(`tr`,{className:e.Archived?`gift-row-disabled`:``,children:[(0,W.jsx)(`td`,{children:e.CoverDocumentID?(0,W.jsx)(fr,{documentID:e.CoverDocumentID,className:`list-thumb`,showError:!1}):(0,W.jsx)(`div`,{className:`sticker-list-thumb-empty`,children:(0,W.jsx)(ke,{size:14})})}),(0,W.jsx)(`td`,{className:`mono`,children:e.ID}),(0,W.jsx)(`td`,{className:`mono`,children:e.ShortName||(0,W.jsx)(`span`,{className:`muted-cell`,children:`None`})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,W.jsx)(`input`,{className:`small-input title-input`,value:h[e.ID]??e.Title,onChange:t=>_(n=>({...n,[e.ID]:t.target.value}))}),(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/rename-sticker-set`,payload:()=>({set_id:e.ID,title:(h[e.ID]??e.Title).trim()}),onDone:()=>void T()})]})}),(0,W.jsx)(`td`,{children:e.Count}),(0,W.jsx)(`td`,{children:e.Official?(0,W.jsx)(q,{tone:`good`,children:`Yes`}):(0,W.jsx)(q,{children:`No`})}),(0,W.jsx)(`td`,{children:e.Archived?(0,W.jsx)(q,{tone:`danger`,children:`Archived`}):(0,W.jsx)(q,{tone:`good`,children:`Enabled`})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,W.jsx)(`input`,{type:`number`,className:`small-input`,value:p[e.ID]??String(e.SortOrder),onChange:t=>m(n=>({...n,[e.ID]:t.target.value}))}),(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/set-sticker-set-sort-order`,payload:()=>({set_id:e.ID,sort_order:Number(p[e.ID]??e.SortOrder)}),onDone:()=>void T()})]})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>y(e),children:[(0,W.jsx)(Se,{size:13}),` `,`View`]}),(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:e.Archived?`Unarchive`:`Archive`,path:`/api/actions/set-sticker-set-archived`,payload:()=>({set_id:e.ID,archived:!e.Archived}),onDone:()=>void T()}),(0,W.jsx)(Z,{compact:!0,tone:`danger`,label:`Delete`,path:`/api/actions/delete-sticker-set`,payload:()=>({set_id:e.ID}),onDone:()=>void T()})]})})]},e.ID)),j.length===0&&(0,W.jsx)(jt,{colSpan:9})]})]})}),l!==`all`&&E.length>0&&(0,W.jsxs)(`div`,{className:`gift-pager`,children:[(0,W.jsx)(`span`,{className:`gift-pager-range`,children:`Showing ${M}-${N} of ${E.length}`}),(0,W.jsxs)(`div`,{className:`gift-pager-controls`,children:[(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>f(e=>Math.max(1,e-1)),disabled:A<=1,children:[(0,W.jsx)(ge,{size:14}),` `,`Previous`]}),(0,W.jsx)(`span`,{className:`gift-pager-page`,children:`Page ${A} of ${D}`}),(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>f(e=>Math.min(D,e+1)),disabled:A>=D,children:[`Next`,` `,(0,W.jsx)(_e,{size:14})]})]})]}),v&&(0,W.jsx)(hr,{set:v,onClose:()=>y(null)}),b&&(0,W.jsx)(pr,{kind:e,onClose:()=>x(!1),onCreated:()=>void T()})]})}var vr=[`Love`,`Approval`,`Disapproval`,`Cheers`,`Laughter`,`Astonishment`,`Sadness`,`Anger`,`Neutral`,`Doubt`,`Silly`];function Q({documentID:e}){let[t,n]=(0,g.useState)(!1);return t?(0,W.jsx)(`div`,{className:`sticker-list-thumb-empty`,children:(0,W.jsx)(ke,{size:14})}):(0,W.jsx)(`video`,{className:`gif-catalog-thumb`,src:k.gifCatalogDocumentPreviewURL(e),muted:!0,loop:!0,autoPlay:!0,playsInline:!0,onError:()=>n(!0)})}function yr(){let[e,t]=(0,g.useState)([]),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(!1),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(10),[u,d]=(0,g.useState)(1),[f,p]=(0,g.useState)({}),[m,h]=(0,g.useState)({}),[_,v]=(0,g.useState)(!1);async function y(){a(!0),s(``);try{t((await k.gifCatalog()).rows??[])}catch(e){s(O(e))}finally{a(!1)}}(0,g.useEffect)(()=>{y()},[]);let b=(0,g.useMemo)(()=>{let t=n.trim().toLowerCase();return t?e.filter(e=>e.ID.includes(t)||e.Title.toLowerCase().includes(t)):e},[e,n]);(0,g.useEffect)(()=>{d(1)},[n,c]);let x=c===`all`?1:Math.max(1,Math.ceil(b.length/c)),S=Math.min(u,x),C=(0,g.useMemo)(()=>{if(c===`all`)return b;let e=(S-1)*c;return b.slice(e,e+c)},[b,S,c]),w=C.length===0?0:c===`all`?1:(S-1)*c+1,T=w===0?0:w+C.length-1,E=(0,g.useMemo)(()=>({total:e.length,enabled:e.filter(e=>e.Enabled).length,uncategorized:e.filter(e=>!e.Category).length}),[e]);return(0,W.jsxs)(Dt,{title:`GIFs`,eyebrow:`Curated GIFs served by @gif in the client's GIF picker (trending + search)`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>y(),disabled:i,children:[(0,W.jsx)(Ke,{size:15}),` `,`Refresh`]}),(0,W.jsx)(Z,{tone:`neutral`,label:`Auto-categorize`,path:`/api/actions/auto-categorize-gif-catalog`,payload:()=>({}),onDone:()=>void y()}),(0,W.jsx)(Z,{tone:`danger`,label:`Delete uncategorized`,path:`/api/actions/delete-uncategorized-gifs`,payload:()=>({}),onDone:()=>void y()}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:()=>v(!0),children:[(0,W.jsx)(Ue,{size:15}),` `,`Add GIF`]})]}),children:[o&&(0,W.jsx)(K,{children:o}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Total GIFs`,value:String(E.total)}),(0,W.jsx)(J,{label:`Enabled`,value:String(E.enabled),tone:`good`}),(0,W.jsx)(J,{label:`Uncategorized`,value:String(E.uncategorized),tone:E.uncategorized>0?`warn`:void 0})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`div`,{className:`toolbar`,children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:`Search ID or title`})]}),(0,W.jsxs)(`label`,{className:`gift-page-size`,children:[(0,W.jsx)(`span`,{children:`Per page`}),(0,W.jsxs)(`select`,{value:String(c),onChange:e=>l(e.target.value===`all`?`all`:Number(e.target.value)),children:[(0,W.jsx)(`option`,{value:`10`,children:`10`}),(0,W.jsx)(`option`,{value:`20`,children:`20`}),(0,W.jsx)(`option`,{value:`50`,children:`50`}),(0,W.jsx)(`option`,{value:`100`,children:`100`}),(0,W.jsx)(`option`,{value:`all`,children:`All`})]})]}),(0,W.jsx)(`span`,{className:`gift-list-summary`,children:`Showing ${b.length} of ${e.length}`})]})}),(0,W.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Preview`}),(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Title`}),(0,W.jsx)(`th`,{children:`Document ID`}),(0,W.jsx)(`th`,{children:`Added by`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Category`}),(0,W.jsx)(`th`,{children:`Sort order`}),(0,W.jsx)(`th`,{children:`Actions`})]})}),(0,W.jsxs)(`tbody`,{children:[C.map(e=>(0,W.jsxs)(`tr`,{className:e.Enabled?``:`gift-row-disabled`,children:[(0,W.jsx)(`td`,{children:(0,W.jsx)(Q,{documentID:e.DocumentID})}),(0,W.jsx)(`td`,{className:`mono`,children:e.ID}),(0,W.jsx)(`td`,{children:e.Title||(0,W.jsx)(`span`,{className:`muted-cell`,children:`Untitled`})}),(0,W.jsx)(`td`,{className:`mono`,children:e.DocumentID}),(0,W.jsx)(`td`,{children:e.CreatedBy||(0,W.jsx)(`span`,{className:`muted-cell`,children:`—`})}),(0,W.jsx)(`td`,{children:e.Enabled?(0,W.jsx)(q,{tone:`good`,children:`Enabled`}):(0,W.jsx)(q,{tone:`danger`,children:`Disabled`})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,W.jsxs)(`select`,{className:`small-input`,value:m[e.ID]??e.Category,onChange:t=>h(n=>({...n,[e.ID]:t.target.value})),children:[(0,W.jsx)(`option`,{value:``,children:`Uncategorized`}),vr.map(e=>(0,W.jsx)(`option`,{value:e,children:e},e))]}),(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/set-gif-catalog-category`,payload:()=>({id:e.ID,category:m[e.ID]??e.Category}),onDone:()=>void y()})]})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`sort-order-editor`,children:[(0,W.jsx)(`input`,{type:`number`,className:`small-input`,value:f[e.ID]??String(e.SortOrder),onChange:t=>p(n=>({...n,[e.ID]:t.target.value}))}),(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:`Save`,path:`/api/actions/set-gif-catalog-sort-order`,payload:()=>({id:e.ID,sort_order:Number(f[e.ID]??e.SortOrder)}),onDone:()=>void y()})]})}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,W.jsx)(Z,{compact:!0,tone:`neutral`,label:e.Enabled?`Disable`:`Enable`,path:`/api/actions/set-gif-catalog-enabled`,payload:()=>({id:e.ID,enabled:!e.Enabled}),onDone:()=>void y()}),(0,W.jsx)(Z,{compact:!0,tone:`danger`,label:`Delete`,path:`/api/actions/delete-gif-catalog-entry`,payload:()=>({id:e.ID}),onDone:()=>void y()})]})})]},e.ID)),C.length===0&&(0,W.jsx)(jt,{colSpan:9})]})]})}),c!==`all`&&b.length>0&&(0,W.jsxs)(`div`,{className:`gift-pager`,children:[(0,W.jsx)(`span`,{className:`gift-pager-range`,children:`Showing ${w}-${T} of ${b.length}`}),(0,W.jsxs)(`div`,{className:`gift-pager-controls`,children:[(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>d(e=>Math.max(1,e-1)),disabled:S<=1,children:[(0,W.jsx)(ge,{size:14}),` `,`Previous`]}),(0,W.jsx)(`span`,{className:`gift-pager-page`,children:`Page ${S} of ${x}`}),(0,W.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>d(e=>Math.min(x,e+1)),disabled:S>=x,children:[`Next`,` `,(0,W.jsx)(_e,{size:14})]})]})]}),_&&(0,W.jsx)(br,{onClose:()=>v(!1),onCreated:()=>void y()})]})}function br({onClose:e,onCreated:t}){let[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``);function m(e){a(e),s(t=>(t&&URL.revokeObjectURL(t),e?URL.createObjectURL(e):null))}async function h(){if(!n.trim()||!i){p(`Title and a GIF/MP4 file are required.`);return}if(!c.trim()){p(`Please enter an operation reason`);return}d(!0),p(``);try{let r=new FormData;r.set(`metadata`,JSON.stringify({command_id:``,reason:c.trim(),confirm:!0,title:n.trim()})),r.set(`file`,i,i.name),await k.createGifCatalogEntry(r),t(),e()}catch(e){p(O(e))}finally{d(!1)}}return(0,on.createPortal)((0,W.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,W.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":`Add a GIF`,children:[(0,W.jsxs)(`div`,{className:`modal-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`eyebrow`,children:`New catalog entry`}),(0,W.jsx)(`h2`,{children:`Add a GIF`})]}),(0,W.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:e,disabled:u,"aria-label":`Close`,children:(0,W.jsx)(ut,{size:15})})]}),(0,W.jsxs)(`div`,{className:`command-body`,children:[(0,W.jsx)(`div`,{className:`gift-fields-grid`,children:(0,W.jsxs)(`label`,{children:[(0,W.jsx)(`span`,{children:`Title`}),(0,W.jsx)(`input`,{value:n,maxLength:128,onChange:e=>r(e.target.value)})]})}),(0,W.jsxs)(`label`,{className:`gift-file-picker ${i?`has-file`:``}`,children:[(0,W.jsx)(`input`,{type:`file`,accept:`.gif,.mp4,image/gif,video/mp4`,onChange:e=>m(e.target.files?.[0]??null)}),(0,W.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,W.jsx)(`span`,{className:`gift-field-label`,children:`File`}),(0,W.jsx)(`strong`,{children:i?i.name:`Choose a GIF or MP4 file`})]}),(0,W.jsx)(`span`,{className:`gift-file-action`,children:i?`Change file`:`Choose file`})]}),o&&(0,W.jsx)(`div`,{className:`gif-catalog-preview`,children:i?.type===`video/mp4`?(0,W.jsx)(`video`,{src:o,autoPlay:!0,loop:!0,muted:!0,playsInline:!0}):(0,W.jsx)(`img`,{src:o,alt:``})}),(0,W.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,W.jsx)(`span`,{children:`Audit reason`}),(0,W.jsx)(`input`,{value:c,placeholder:`Briefly describe why this GIF is being added`,onChange:e=>l(e.target.value)})]}),f&&(0,W.jsx)(K,{children:f})]}),(0,W.jsxs)(`div`,{className:`modal-actions`,children:[(0,W.jsx)(`button`,{className:`btn`,type:`button`,onClick:e,disabled:u,children:`Close`}),(0,W.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:h,disabled:u,children:[u?(0,W.jsx)(L,{className:`spin`,size:15}):(0,W.jsx)(ot,{size:15}),`Add GIF`]})]})]})}),document.body)}var xr=`open,in_review,action_pending,action_failed,appeal_review`,Sr=[{value:xr,label:`Active queue`},{value:`open,in_review,action_pending,action_failed,resolved,dismissed,appeal_review`,label:`All statuses`},{value:`open`,label:`Open`},{value:`in_review`,label:`In review`},{value:`action_pending`,label:`Action pending`},{value:`action_failed`,label:`Action failed`},{value:`appeal_review`,label:`Appeal review`},{value:`resolved`,label:`Resolved`},{value:`dismissed`,label:`Dismissed`}];function Cr({navigate:e}){let[t,n]=(0,g.useState)(xr),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),u(``);try{let e=new URLSearchParams({statuses:t,limit:`100`});r.trim()&&e.set(`assigned_to`,r.trim()),o((await k.moderationCases(e)).cases)}catch(e){u(O(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d()},[]);let f=a.filter(e=>e.Status===`action_pending`||e.Status===`action_failed`).length,p=a.filter(e=>e.Severity===4).length;return(0,W.jsxs)(Dt,{title:`Reports and Moderation`,eyebrow:`Moderation / Cases`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:d,disabled:s,children:[(0,W.jsx)(Ke,{size:15,className:s?`spin`:``}),` `,`Refresh`]}),children:[l&&(0,W.jsx)(K,{children:l}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Current queue`,value:String(a.length)}),(0,W.jsx)(J,{label:`Critical cases`,value:String(p),tone:p?`danger`:`neutral`}),(0,W.jsx)(J,{label:`Pending / failed actions`,value:String(f),tone:f?`warn`:`good`})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),d()},children:[(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Status`}),(0,W.jsx)(`select`,{"aria-label":`Case status filter`,value:t,onChange:e=>n(e.target.value),children:Sr.map(e=>(0,W.jsx)(`option`,{value:e.value,children:e.label},e.value))})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Reviewer`}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`Leave blank for all`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:s,children:[(0,W.jsx)(Xe,{size:15}),` `,`Search`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Case`}),(0,W.jsx)(`th`,{children:`Target`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Severity`}),(0,W.jsx)(`th`,{children:`Reports / Reporters`}),(0,W.jsx)(`th`,{children:`Reviewer`}),(0,W.jsx)(`th`,{children:`Latest report`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[a.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsxs)(`td`,{className:`mono`,children:[`#`,t.ID]}),(0,W.jsx)(`td`,{className:`mono`,children:kr(t.Target.Type,t.Target.ID)}),(0,W.jsx)(`td`,{children:(0,W.jsx)(wr,{status:t.Status})}),(0,W.jsx)(`td`,{children:(0,W.jsx)(Er,{value:t.Severity})}),(0,W.jsxs)(`td`,{children:[t.ReportCount,` / `,t.DistinctReporterCount]}),(0,W.jsx)(`td`,{children:t.AssignedTo||`-`}),(0,W.jsx)(`td`,{children:U(t.LastReportAt)}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/moderation/${t.ID}`),children:[`Review`,` `,(0,W.jsx)(_e,{size:14})]})})]},t.ID)),a.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})})]})}function wr({status:e}){return(0,W.jsx)(q,{tone:e===`resolved`||e===`dismissed`?`good`:e===`action_failed`?`danger`:e===`action_pending`?`warn`:`neutral`,children:Or(`status`,e)})}var Tr={low:`Low`,medium:`Medium`,high:`High`,critical:`Critical`};function Er({value:e}){let t=[``,`low`,`medium`,`high`,`critical`][e];return(0,W.jsx)(q,{tone:e>=4?`danger`:e>=3?`warn`:`neutral`,children:t?Tr[t]:e})}var Dr={status:{open:`Open`,in_review:`In review`,action_pending:`Action pending`,action_failed:`Action failed`,appeal_review:`Appeal review`,resolved:`Resolved`,dismissed:`Dismissed`},targetType:{channel:`Channel`,chat:`Group`,user:`Account`},source:{account_peer:`Account / peer`,antispam_false_positive:`Anti-spam false positive`,channel_spam:`Channel spam`,encrypted_spam:`Encrypted-chat spam`,ephemeral:`Ephemeral media`,messages:`Messages`,messages_spam:`Message spam`,profile_photo:`Profile photo`,reaction:`Reaction`,sponsored:`Sponsored message`,story:`Story`},reason:{child_abuse:`Child abuse`,copyright:`Copyright`,fake:`Fake`,geo_irrelevant:`Location-irrelevant`,illegal_drugs:`Illegal drugs`,other:`Other`,personal_details:`Personal details`,pornography:`Pornography`,spam:`Spam`,violence:`Violence`}};function Or(e,t){return Dr[e]?.[t]??t}function kr(e,t){return`${Or(`targetType`,e)} #${t}`}function Ar({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(`no_violation`),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!0),[_,v]=(0,g.useState)(!1),[y,b]=(0,g.useState)(``);function x(e){a(e),e&&(d(e.Items.filter(e=>e.Kind===`message`).map(e=>Number(e.ItemID)).filter(e=>Number.isSafeInteger(e)&&e>0).join(`, `)),p(String(e.ReporterUserID)))}async function S(){b(``);try{let t=await k.moderationCase(e);r(t);let n=t.ReportIDs[0];x(n?await k.moderationReport(n):null)}catch(e){b(O(e))}}(0,g.useEffect)(()=>{S()},[e]);let C=(0,g.useMemo)(()=>jr(c,n?.Case.Target.Type,Mr(u),Number(f),m),[c,n?.Case.Target.Type,u,f,m]),w=(0,g.useMemo)(()=>n?Nr(n):{actions:[],label:`None`,blocked:!1},[n]);async function T(){if(n){v(!0),b(``);try{await k.claimModerationCase(e,n.Case.Version),await S()}catch(e){b(O(e))}finally{v(!1)}}}async function E(){if(!n||!o.trim()){b(`A review reason is required.`);return}if(c===`delete_messages`&&C.length===0){b(n.Case.Target.Type===`user`?`Private-message deletion requires valid evidence message IDs and the reporter's owner_user_id.`:`Channel-message deletion requires at least one valid evidence message ID.`);return}if(window.confirm(`Submit the “${Pr(c)}” decision? The action will run through the durable action queue.`)){v(!0),b(``);try{r((await k.decideModerationCase(e,{expected_version:n.Case.Version,reason:o.trim(),kind:c===`no_violation`?`no_violation`:`violation`,actions:C})).case),s(``)}catch(e){b(O(e))}finally{v(!1)}}}async function D(t,i){if(!n||!o.trim()){b(`An appeal review reason is required.`);return}if(window.confirm(i?`Grant this appeal?`:`Deny this appeal?`)){v(!0);try{r((await k.reviewModerationAppeal(e,t,{expected_version:n.Case.Version,reason:o.trim(),granted:i,actions:i?w.actions:[]})).case),s(``)}catch(e){b(O(e))}finally{v(!1)}}}if(y&&!n)return(0,W.jsx)(K,{children:y});if(!n)return(0,W.jsx)(X,{label:`Loading moderation case…`});let A=n.Case,j=A.Status===`open`||A.Status===`in_review`||A.Status===`appeal_review`,M=(A.Status===`in_review`||A.Status===`action_failed`)&&!!A.AssignedTo,N=M&&(A.Status!==`action_failed`||c!==`no_violation`),P=n.Appeals.find(e=>e.Status===`pending`);return(0,W.jsxs)(Dt,{title:`Review case #${A.ID}`,eyebrow:`Moderation / Case detail`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/moderation`),children:[(0,W.jsx)(le,{size:15}),` `,`Back to queue`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,onClick:S,children:[(0,W.jsx)(Ke,{size:15}),` `,`Refresh`]})]}),children:[y&&(0,W.jsx)(K,{children:y}),(0,W.jsx)(kt,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:kr(A.Target.Type,A.Target.ID)}),(0,W.jsx)(`div`,{className:`entity-subtitle`,children:`Version ${A.Version} · Updated ${U(A.UpdatedAt)}`})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(wr,{status:A.Status}),(0,W.jsx)(Er,{value:A.Severity})]})]}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Target`,value:kr(A.Target.Type,A.Target.ID),mono:!0}),(0,W.jsx)(Y,{label:`Reports`,value:`${A.ReportCount} reports from ${A.DistinctReporterCount} reporters`}),(0,W.jsx)(Y,{label:`Reviewer`,value:A.AssignedTo||`-`}),(0,W.jsx)(Y,{label:`First / latest report`,value:`${U(A.FirstReportAt)} / ${U(A.LastReportAt)}`})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Report evidence`,text:`Shows up to the latest 100 reports; snapshots are frozen when reports are admitted.`}),(0,W.jsx)(`div`,{className:`toolbar`,children:n.ReportIDs.map(e=>(0,W.jsxs)(`button`,{className:`btn`,onClick:async()=>x(await k.moderationReport(e)),children:[`#`,e]},e))}),i&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Source / Reason`,value:`${Or(`source`,i.Source)} / ${Or(`reason`,i.Reason)}`}),(0,W.jsx)(Y,{label:`Reporter`,value:String(i.ReporterUserID),mono:!0}),(0,W.jsx)(Y,{label:`Option`,value:i.Option,mono:!0}),(0,W.jsx)(Y,{label:`Time`,value:U(i.CreatedAt)})]}),i.Comment&&(0,W.jsx)(`p`,{className:`about-text`,children:i.Comment}),(0,W.jsx)(Mt,{value:JSON.stringify(i,null,2)})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Decision and action audit`,text:`Actions run idempotently through a lease worker; failures retain their error and attempt count.`}),(0,W.jsx)(Mt,{value:JSON.stringify({decisions:n.Decisions,actions:n.Actions},null,2)})]}),n.Appeals.length>0&&(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Appeals`}),(0,W.jsx)(Mt,{value:JSON.stringify(n.Appeals,null,2)})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:`Case actions`}),j&&(0,W.jsxs)(`button`,{className:`btn primary icon-text`,disabled:_,onClick:T,children:[(0,W.jsx)(Ze,{size:15}),` `,A.AssignedTo?`Renew claim`:`Claim case`]}),(0,W.jsxs)(`label`,{className:`field`,children:[(0,W.jsx)(`span`,{children:`Review reason`}),(0,W.jsx)(`textarea`,{value:o,onChange:e=>s(e.target.value),rows:5})]}),(0,W.jsxs)(`label`,{className:`field`,children:[(0,W.jsx)(`span`,{children:`Decision template`}),(0,W.jsxs)(`select`,{value:c,onChange:e=>l(e.target.value),children:[(0,W.jsx)(`option`,{value:`no_violation`,children:`No violation (dismiss report)`}),(0,W.jsx)(`option`,{value:`scam`,children:`Mark as SCAM`}),(0,W.jsx)(`option`,{value:`fake`,children:`Mark as FAKE`}),(0,W.jsx)(`option`,{value:`freeze`,children:`Freeze account`}),(0,W.jsx)(`option`,{value:`scam_freeze`,children:`SCAM + freeze`}),(0,W.jsx)(`option`,{value:`fake_freeze`,children:`FAKE + freeze`}),(0,W.jsx)(`option`,{value:`delete_messages`,children:`Delete messages covered by evidence`}),(0,W.jsx)(`option`,{value:`delete_account`,children:`Delete account`})]})]}),c===`delete_messages`&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`label`,{className:`field`,children:[(0,W.jsx)(`span`,{children:`Evidence message IDs (comma-separated)`}),(0,W.jsx)(`input`,{value:u,onChange:e=>d(e.target.value),placeholder:`101, 102`})]}),A.Target.Type===`user`&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`label`,{className:`field`,children:[(0,W.jsx)(`span`,{children:`Private-chat owner_user_id`}),(0,W.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),inputMode:`numeric`})]}),(0,W.jsxs)(`label`,{className:`field checkbox-field`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),(0,W.jsx)(`span`,{children:`Revoke for both sides`})]})]}),(0,W.jsx)(K,{children:`The server will verify again that every message ID exists in this case's immutable report evidence.`})]}),A.Status===`action_failed`&&c===`no_violation`&&(0,W.jsx)(K,{children:`The action was partially executed and cannot be changed directly to no violation. Select a new action to retry while retaining the previous failure audit.`}),M&&(0,W.jsxs)(`button`,{className:`btn danger icon-text`,disabled:_||!N,onClick:E,children:[(0,W.jsx)(I,{size:15}),` `,A.Status===`action_failed`?`Retry action`:`Submit decision`]}),P&&A.AssignedTo&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsx)(`div`,{className:`dock-title`,children:`Appeal review #${P.ID}`}),(0,W.jsx)(Y,{label:`Automatic remedy after approval`,value:w.label}),w.blocked&&(0,W.jsx)(K,{children:`The case contains a completed irreversible deletion. It cannot be marked as approved and restored; deny it or escalate for manual handling.`}),(0,W.jsx)(`button`,{className:`btn`,disabled:_,onClick:()=>D(P.ID,!1),children:`Deny appeal`}),(0,W.jsx)(`button`,{className:`btn primary`,disabled:_||w.blocked,onClick:()=>D(P.ID,!0),children:`Grant appeal`})]})]})})]})}function jr(e,t,n,r,i){switch(e){case`scam`:return[{kind:`mark_scam`,payload:{}}];case`fake`:return[{kind:`mark_fake`,payload:{}}];case`freeze`:return[{kind:`freeze_account`,payload:{}}];case`scam_freeze`:return[{kind:`mark_scam`,payload:{}},{kind:`freeze_account`,payload:{}}];case`fake_freeze`:return[{kind:`mark_fake`,payload:{}},{kind:`freeze_account`,payload:{}}];case`delete_messages`:return n.length===0?[]:t===`channel`?[{kind:`delete_channel_message`,payload:{ids:n}}]:t===`user`&&Number.isSafeInteger(r)&&r>0?[{kind:`delete_private_message`,payload:{owner_user_id:r,ids:n,revoke:i}}]:[];case`delete_account`:return[{kind:`delete_account`,payload:{}}];default:return[]}}function Mr(e){let t=e.split(/[,\s]+/).filter(Boolean).map(Number);return t.length===0||t.some(e=>!Number.isSafeInteger(e)||e<=0)?[]:[...new Set(t)]}function Nr(e){let t=!1,n=!1,r=!1;for(let i of[...e.Actions].sort((e,t)=>e.ID-t.ID))if(i.Status===`succeeded`)switch(i.Kind){case`mark_scam`:case`mark_fake`:t=!0;break;case`clear_peer_flags`:t=!1;break;case`freeze_account`:n=!0;break;case`unfreeze_account`:n=!1;break;case`delete_private_message`:case`delete_channel_message`:case`delete_account`:r=!0;break}let i=[],a=[];return t&&(i.push({kind:`clear_peer_flags`,payload:{}}),a.push(`Clear SCAM / FAKE`)),n&&(i.push({kind:`unfreeze_account`,payload:{}}),a.push(`Unfreeze account`)),{actions:i,label:a.join(` + `)||`No recovery action needed`,blocked:r}}function Pr(e){return{no_violation:`No violation (dismiss report)`,scam:`Mark as SCAM`,fake:`Mark as FAKE`,freeze:`Freeze account`,scam_freeze:`SCAM + freeze`,fake_freeze:`FAKE + freeze`,delete_messages:`Delete messages covered by evidence`,delete_account:`Delete account`}[e]}function Fr({navigate:e}){let[t,n]=(0,g.useState)(null),[r,i]=(0,g.useState)([]),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(0),[l,u]=(0,g.useState)(!1),[d,f]=(0,g.useState)(``);async function p(){try{n(await k.storageStats())}catch{}}async function m(e=!1){u(!0),f(``);let t=new URLSearchParams({limit:`50`,offset:String(e?s:0)});try{let n=await k.storageAccounts(t),r=n.rows??[];i(t=>e?[...t,...r]:r),c(n.next_offset),o(!!n.has_more)}catch(e){f(O(e))}finally{u(!1)}}function h(){p(),m(!1)}(0,g.useEffect)(()=>{h()},[]);let _=t?Math.max(0,Number(t.LogicalBytes)-Number(t.PhysicalBytes)):0;return(0,W.jsxs)(Dt,{title:`Storage`,eyebrow:`Media / Storage usage`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:h,disabled:l,children:[(0,W.jsx)(Ke,{size:15,className:l?`spin`:``}),` `,`Refresh`]}),children:[d&&(0,W.jsx)(K,{children:d}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Physical usage (on disk / S3)`,value:t?wt(t.PhysicalBytes):`-`}),(0,W.jsx)(J,{label:`Logical usage (sum per account)`,value:t?wt(t.LogicalBytes):`-`}),(0,W.jsx)(J,{label:`Saved by dedup`,value:wt(String(_)),tone:_>0?`good`:`neutral`}),(0,W.jsx)(J,{label:`Backend`,value:t?.BackendKind??`-`})]}),(0,W.jsxs)(`div`,{className:`metric-row`,children:[(0,W.jsx)(J,{label:`Documents`,value:t?_t(t.DocumentCount):`-`}),(0,W.jsx)(J,{label:`Photos`,value:t?_t(t.PhotoCount):`-`}),(0,W.jsx)(J,{label:`Accounts with media`,value:t?_t(t.AccountCount):`-`}),(0,W.jsx)(J,{label:`Unattributed`,value:t?wt(t.UnattributedBytes):`-`,tone:t&&Number(t.UnattributedBytes)>0?`warn`:`neutral`})]}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`User ID`}),(0,W.jsx)(`th`,{children:`Account`}),(0,W.jsx)(`th`,{children:`Storage used`}),(0,W.jsx)(`th`,{children:`Files`})]})}),(0,W.jsxs)(`tbody`,{children:[r.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:e.UserID}),(0,W.jsx)(`td`,{children:H(e.Username)||e.FirstName||`-`}),(0,W.jsx)(`td`,{className:`mono`,children:wt(e.Bytes)}),(0,W.jsx)(`td`,{className:`mono`,children:_t(e.FileCount)})]},e.UserID)),r.length===0&&(0,W.jsx)(jt,{colSpan:4})]})]})}),a&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:l,children:[l?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(he,{size:15}),` `,`Load more`]})})]})}function Ir({loader:e,cacheKey:t,className:n,playOnHover:r=!0,onError:i}){let a=(0,g.useRef)(null),o=(0,g.useRef)(null);(0,g.useEffect)(()=>{let t=!1;return e().then(e=>{t||!a.current||(o.current?.destroy(),o.current=dr.default.loadAnimation({container:a.current,renderer:`canvas`,loop:!0,autoplay:!1,animationData:structuredClone(e)}),o.current.goToAndStop(0,!0))}).catch(()=>i?.()),()=>{t=!0,o.current?.destroy(),o.current=null}},[t]);function s(){r&&o.current?.play()}function c(){r&&o.current?.goToAndStop(0,!0)}return(0,W.jsx)(`div`,{className:n,ref:a,onMouseEnter:s,onMouseLeave:c})}function Lr(e){let t=e.toLowerCase();return t.includes(`tgsticker`)||t.includes(`lottie`)||t.includes(`json`)}function Rr({row:e}){let[t,n]=(0,g.useState)(!Lr(e.MimeType));return(0,g.useEffect)(()=>{n(!Lr(e.MimeType))},[e.DocumentID,e.MimeType]),t?(0,W.jsx)(`div`,{className:`emoji-picker-glyph`,children:e.Alt||`🙂`}):(0,W.jsx)(Ir,{className:`emoji-picker-anim`,cacheKey:e.DocumentID,loader:()=>k.emojiAnimation(e.DocumentID),onError:()=>n(!0)})}function zr({label:e,value:t,onChange:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)([]),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``),d=a.find(e=>e.DocumentID===t)??null;async function f(){c(!0),u(``);let e=new URLSearchParams({limit:`24`});r.trim()&&e.set(`q`,r.trim());try{o((await k.emoji(e)).rows??[])}catch(e){u(O(e))}finally{c(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,W.jsxs)(`div`,{className:`entity-picker`,children:[(0,W.jsxs)(`div`,{className:`picker-head`,children:[(0,W.jsx)(`span`,{children:e}),t?(0,W.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(``),children:[(0,W.jsx)(ut,{size:13}),` `,`Clear`]}):null]}),t?(0,W.jsxs)(`div`,{className:`selected-entity`,children:[(0,W.jsx)(me,{size:15}),(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`strong`,{children:d?.Alt||`—`}),(0,W.jsx)(`span`,{className:`mono`,children:t})]}),(0,W.jsx)(`span`,{children:d?.SetTitle||`-`})]}):null,(0,W.jsxs)(`div`,{className:`picker-search`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:`Search document ID or emoji`}),(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:s,children:s?(0,W.jsx)(L,{size:14,className:`spin`}):`Search`})]}),l&&(0,W.jsx)(`div`,{className:`picker-error`,children:l}),(0,W.jsxs)(`div`,{className:`picker-results emoji-picker-results`,children:[a.map(e=>(0,W.jsxs)(`button`,{className:`picker-row emoji-picker-row ${t===e.DocumentID?`selected`:``}`,type:`button`,onClick:()=>n(e.DocumentID),children:[(0,W.jsx)(Rr,{row:e}),(0,W.jsx)(`span`,{className:`mono`,children:e.DocumentID}),(0,W.jsx)(`span`,{children:e.SetTitle||`—`})]},e.DocumentID)),a.length===0&&!s?(0,W.jsx)(`div`,{className:`picker-empty`,children:`No results`}):null]})]})}var Br=[`pending`,`approved`,`rejected`,`revoked`],Vr=[`user`,`channel`],Hr={pending:`Pending`,approved:`Approved`,rejected:`Rejected`,revoked:`Mark revoked`},Ur={user:`Account`,channel:`Channel`};function Wr({navigate:e}){let{can:t}=zt(),n=t(It),r=t(Pt),[i,a]=(0,g.useState)(`requests`),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)([]),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(!1);async function m(){d(``),p(!1);try{let[e,t]=await Promise.all([k.botVerifiers(new URLSearchParams({limit:`200`})),k.verificationIcons(new URLSearchParams({limit:`200`}))]);s(e.rows??[]),l(t.rows??[])}catch(e){if(e instanceof v&&e.status===403){s([]),l([]),p(!0);return}d(O(e))}}return(0,g.useEffect)(()=>{m()},[]),(0,W.jsxs)(Dt,{title:`Third-party verification`,eyebrow:`Third-party verification / Verifiers, icons, marks`,actions:r?(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>e(`/verification`),children:[(0,W.jsx)(xe,{size:15}),` `,`Official verification`]}):void 0,children:[u&&(0,W.jsx)(K,{children:u}),f&&(0,W.jsx)(K,{children:`The server refused the verifier roster and the icon catalogue for this session (403), so both lists are empty here — applications can still be reviewed.`}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`A verifier company's icon — not the official checkmark`,text:`A third-party mark is a verifier bot's own icon, drawn right BEFORE the name of an account, a bot or a channel, plus one line of description in the profile. It says “this verifier vouches for this peer”, and nothing more.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The icon is a custom emoji document. The client fetches it through messages.getCustomEmojiDocuments, so a document id that resolves to nothing renders as no badge at all — which is why marks are granted from the catalogue below rather than from a typed number.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The official checkmark is a different mechanism, granted by the platform in the Verification section. The two are stored, shown and taken away separately, and neither one implies the other.`}),!n&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This session can read the section and decide applications, but not change verifiers or the icon catalogue — that needs the botverification.manage permission.`})]}),(0,W.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":`Third-party verification`,children:[{key:`requests`,label:`Applications`,icon:(0,W.jsx)(tt,{size:15})},{key:`verifiers`,label:`Verifiers`,icon:(0,W.jsx)(fe,{size:15})},{key:`icons`,label:`Icon catalogue`,icon:(0,W.jsx)(nt,{size:15})},{key:`marks`,label:`Granted marks`,icon:(0,W.jsx)(F,{size:15})}].map(e=>(0,W.jsxs)(`button`,{className:`btn icon-text ${i===e.key?`primary`:``}`,type:`button`,"aria-pressed":i===e.key,onClick:()=>a(e.key),children:[e.icon,` `,e.label]},e.key))}),i===`requests`&&(0,W.jsx)(Gr,{navigate:e,verifiers:o}),i===`verifiers`&&(0,W.jsx)(Kr,{verifiers:o,icons:c,canManage:n,onChanged:m,navigate:e}),i===`icons`&&(0,W.jsx)(qr,{icons:c,verifiers:o,canManage:n,onChanged:m}),i===`marks`&&(0,W.jsx)(Jr,{verifiers:o,canManage:n,navigate:e})]})}function Gr({navigate:e,verifiers:t}){let[n,r]=(0,g.useState)(`pending`),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(`all`),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(`50`),[f,p]=(0,g.useState)([]),[m,h]=(0,g.useState)({}),[_,v]=(0,g.useState)(!1),[y,b]=(0,g.useState)(``),[x,S]=(0,g.useState)(!1),[C,w]=(0,g.useState)(``);async function T(e=!1){S(!0),w(``);let t=new URLSearchParams({limit:u});n!==`all`&&t.set(`status`,n),i&&t.set(`verifier_bot_id`,i),o!==`all`&&t.set(`peer_type`,o),c.trim()&&t.set(`q`,c.trim().replace(/^@/,``)),e&&y&&t.set(`before_id`,y);try{let n=await k.customVerificationRequests(t),r=n.rows??[];p(t=>e?[...t,...r]:r),b(n.next_before_id??``),v(!!n.has_more)}catch(e){w(O(e))}finally{S(!1)}}async function E(){try{h((await k.botVerificationCounts()).counts??{})}catch(e){w(O(e))}}(0,g.useEffect)(()=>{T(!1),E()},[]);function D(){T(!1),E()}return(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Application queue`,text:`Applications filed with a verifier bot by the owner of the peer. The counters cover the whole queue, not the page below.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:D,disabled:x,children:[(0,W.jsx)(Ke,{size:15,className:x?`spin`:``}),` `,`Refresh`]})}),C&&(0,W.jsx)(K,{children:C}),(0,W.jsx)(`div`,{className:`metric-row`,children:Br.map(e=>(0,W.jsx)(J,{label:Hr[e],value:m[e]??`0`,mono:!0,tone:Qr(e,m[e]??`0`)},e))})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),T(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:`Application id, peer id, username or title`})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Status`}),(0,W.jsxs)(`select`,{value:n,onChange:e=>r(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All statuses`}),Br.map(e=>(0,W.jsx)(`option`,{value:e,children:Hr[e]},e))]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Verifier`}),(0,W.jsx)(Yr,{value:i,verifiers:t,onChange:a})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Peer type`}),(0,W.jsxs)(`select`,{value:o,onChange:e=>s(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All types`}),Vr.map(e=>(0,W.jsx)(`option`,{value:e,children:Ur[e]},e))]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:x,children:[x?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,`Search`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Verifier`}),(0,W.jsx)(`th`,{children:`Peer`}),(0,W.jsx)(`th`,{children:`Applicant`}),(0,W.jsx)(`th`,{children:`Stated reason`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Filed`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[f.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:(0,W.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/bot-verification/${t.ID}`),children:[`#`,t.ID]})}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{children:H(t.VerifierBotUsername)||t.VerifierBotID}),(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:t.VerifierBotID})]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{children:$r(t)}),(0,W.jsxs)(`div`,{className:`entity-subtitle mono`,children:[Ur[t.PeerType],` · `,t.PeerID]})]}),(0,W.jsxs)(`td`,{children:[H(t.ApplicantUsername)||`-`,(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:t.ApplicantUserID})]}),(0,W.jsx)(`td`,{className:`truncate`,children:t.Reason||`-`}),(0,W.jsx)(`td`,{children:(0,W.jsx)(Xr,{status:t.Status})}),(0,W.jsx)(`td`,{children:U(t.CreatedAt)||`-`}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/bot-verification/${t.ID}`),children:[(0,W.jsx)(tt,{size:14}),` `,`Details`,` `,(0,W.jsx)(_e,{size:14})]})})]},t.ID)),f.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})}),_&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>T(!0),disabled:x,children:[x?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(he,{size:15}),` `,`Load more`]})})]})}function Kr({verifiers:e,icons:t,canManage:n,onChanged:r,navigate:i}){let[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(``),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(``),[h,_]=(0,g.useState)(!1),v=t.filter(e=>e.Active),y=v.map(e=>({value:e.DocumentID,label:`${e.Name} · ${e.DocumentID}`}));if(l&&!y.some(e=>e.value===l)){let e=t.find(e=>e.DocumentID===l);y.unshift({value:l,label:`${e?.Name??l} · ${l} (Retired)`})}function b(e){c(e),o(null),u(e.IconDocumentID),f(e.CompanyName),m(e.DefaultDescription),_(e.CanModifyCustomDescription)}function x(){c(null),o(null),u(``),f(``),m(``),_(!1)}function S(){return{bot_id:s?s.BotID:a?String(a.ID):`0`,icon_document_id:l||`0`,company_name:d.trim(),default_description:p.trim(),can_modify_custom_description:h,version:s?s.Version:`0`}}return(0,W.jsxs)(W.Fragment,{children:[n&&(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:s?`Update verifier`:`Grant verifier status`,text:`The bot gets an icon from the catalogue and a company name to vouch under. The same call updates an existing verifier, which is why it carries a version.`,action:s?(0,W.jsx)(`button`,{className:`btn icon-text`,type:`button`,onClick:x,children:`Cancel update`}):void 0}),s?(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Updating ${H(s.BotUsername)||s.BotID} — version ${s.Version} is sent as the optimistic lock, so a row somebody else changed meanwhile is refused instead of overwritten.`}):(0,W.jsx)(Nn,{label:`Bot`,value:a,onChange:o}),(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Icon from the catalogue`}),(0,W.jsxs)(`select`,{value:l,onChange:e=>u(e.target.value),children:[(0,W.jsx)(`option`,{value:``,children:`Pick an icon`}),y.map(e=>(0,W.jsx)(`option`,{value:e.value,children:e.label},e.value))]})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Company`}),(0,W.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),placeholder:`Acme Verification Ltd`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Default description`}),(0,W.jsx)(`input`,{value:p,onChange:e=>m(e.target.value),placeholder:`Verified by Acme`})]})]}),(0,W.jsxs)(`label`,{className:`checkline`,children:[(0,W.jsx)(`input`,{type:`checkbox`,checked:h,onChange:e=>_(e.target.checked)}),`The verifier may replace the description per peer`]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This is botVerifierSettings.can_modify_custom_description: with it off, every mark this verifier grants carries the default description above, whatever the applicant asked for.`}),v.length===0&&(0,W.jsx)(K,{children:`The catalogue has no active icon, so there is nothing to grant. Add one in the icon catalogue first.`}),(0,W.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,W.jsx)(`span`,{className:`bot-create-note`,children:`The bot can mark peers as soon as the row exists and is enabled.`}),(0,W.jsx)(Z,{label:s?`Update verifier`:`Grant verifier status`,icon:(0,W.jsx)(Ue,{size:15}),tone:`neutral`,path:`/api/actions/grant-bot-verifier`,payload:S,onDone:()=>{x(),r()}})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Verifier bots`,text:`Bots allowed to hand out their own mark. Verifier status is granted per deployment, so every row here is a badge printer an operator switched on by hand.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:r,children:[(0,W.jsx)(Ke,{size:15}),` `,`Refresh`]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Bot`}),(0,W.jsx)(`th`,{children:`Company`}),(0,W.jsx)(`th`,{children:`Icon`}),(0,W.jsx)(`th`,{children:`Own description`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Marks`}),(0,W.jsx)(`th`,{children:`Granted by`}),(0,W.jsx)(`th`,{children:`Updated`}),n&&(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[e.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`button`,{className:`row-link`,type:`button`,onClick:()=>i(`/bots/${e.BotID}`),children:(0,W.jsx)(`strong`,{children:H(e.BotUsername)||e.BotName||e.BotID})}),(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:e.BotID})]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{children:e.CompanyName||`-`}),(0,W.jsx)(`div`,{className:`entity-subtitle truncate`,children:e.DefaultDescription||`Not set`})]}),(0,W.jsxs)(`td`,{children:[e.IconName||`-`,(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:e.IconDocumentID})]}),(0,W.jsx)(`td`,{children:e.CanModifyCustomDescription?`Yes`:`No`}),(0,W.jsx)(`td`,{children:e.Enabled?(0,W.jsx)(q,{tone:`good`,children:`Enabled`}):(0,W.jsx)(q,{tone:`warn`,children:`disabled`})}),(0,W.jsx)(`td`,{className:`mono`,children:String(e.MarkCount??`0`)}),(0,W.jsxs)(`td`,{children:[e.GrantedBy||`-`,(0,W.jsx)(`div`,{className:`entity-subtitle truncate`,children:e.GrantReason||`-`})]}),(0,W.jsx)(`td`,{children:U(e.UpdatedAt)||`-`}),n&&(0,W.jsx)(`td`,{children:(0,W.jsxs)(`div`,{className:`row-actions`,children:[(0,W.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>b(e),children:`Edit`}),(0,W.jsx)(Z,{label:e.Enabled?`Disable`:`Enable`,icon:e.Enabled?(0,W.jsx)(We,{size:14}):(0,W.jsx)(B,{size:14}),tone:e.Enabled?`warn`:`neutral`,compact:!0,path:`/api/actions/set-bot-verifier-enabled`,payload:()=>({bot_id:e.BotID,enabled:!e.Enabled}),onDone:r}),(0,W.jsx)(Z,{label:`Revoke status`,icon:(0,W.jsx)(it,{size:14}),tone:`danger`,compact:!0,path:`/api/actions/revoke-bot-verifier`,payload:()=>({bot_id:e.BotID}),onDone:r})]})})]},e.BotID)),e.length===0&&(0,W.jsx)(jt,{colSpan:n?9:8})]})]})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Disabling is the per-verifier kill switch: the marks already granted keep rendering, but the bot can no longer mark anything new and its settings stop being projected into botInfo.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Revoking verifier status removes the row and every mark this verifier granted — the icon disappears from all of its peers at once.`})]})]})}function qr({icons:e,verifiers:t,canManage:n,onChanged:r}){let[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``);function u(){let e={document_id:i.trim()||`0`,name:o.trim()};return c&&(e.owner_bot_id=c),e}return(0,W.jsxs)(W.Fragment,{children:[n&&(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{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.`}),(0,W.jsx)(zr,{label:`Document`,value:i,onChange:a}),(0,W.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Name`}),(0,W.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:`Acme blue tick`})]}),(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Owner`}),(0,W.jsxs)(`select`,{value:c,onChange:e=>l(e.target.value),children:[(0,W.jsx)(`option`,{value:``,children:`Shared`}),t.map(e=>(0,W.jsx)(`option`,{value:e.BotID,children:`${e.CompanyName||e.BotID} · ${H(e.BotUsername)||e.BotID}`},e.BotID))]})]})]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`A document id that resolves to nothing produces an invisible badge: the peer is marked in the database and the client draws nothing.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`A shared icon may be granted to any verifier; picking an owner reserves it for that one bot.`}),(0,W.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,W.jsx)(`span`,{className:`bot-create-note`,children:`Adding an icon grants nothing by itself — it only makes the document available to grant.`}),(0,W.jsx)(Z,{label:`Save icon`,icon:(0,W.jsx)(Ue,{size:15}),tone:`neutral`,path:`/api/actions/upsert-verification-icon`,payload:u,onDone:()=>{a(``),s(``),l(``),r()}})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Icon catalogue`,text:`The custom emoji documents a verifier may mark with. Nothing else can be used as an icon, so the catalogue is where a wrong badge is prevented rather than fixed.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:r,children:[(0,W.jsx)(Ke,{size:15}),` `,`Refresh`]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Document ID`}),(0,W.jsx)(`th`,{children:`Name`}),(0,W.jsx)(`th`,{children:`Owner`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Verifiers using it`}),(0,W.jsx)(`th`,{children:`Filed`}),n&&(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[e.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:e.DocumentID}),(0,W.jsx)(`td`,{children:(0,W.jsx)(`strong`,{children:e.Name||`-`})}),(0,W.jsx)(`td`,{children:e.OwnerBotID&&e.OwnerBotID!==`0`?(0,W.jsxs)(W.Fragment,{children:[H(e.OwnerBotUsername)||e.OwnerBotID,(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:e.OwnerBotID})]}):(0,W.jsx)(q,{children:`Shared`})}),(0,W.jsx)(`td`,{children:e.Active?(0,W.jsx)(q,{tone:`good`,children:`Active`}):(0,W.jsx)(q,{tone:`warn`,children:`Retired`})}),(0,W.jsx)(`td`,{className:`mono`,children:String(e.UsedByVerifiers??`0`)}),(0,W.jsx)(`td`,{children:U(e.CreatedAt)||`-`}),n&&(0,W.jsx)(`td`,{children:(0,W.jsx)(`div`,{className:`row-actions`,children:(0,W.jsx)(Z,{label:e.Active?`Retire`:`Activate`,icon:e.Active?(0,W.jsx)(We,{size:14}):(0,W.jsx)(B,{size:14}),tone:e.Active?`warn`:`neutral`,compact:!0,path:`/api/actions/set-verification-icon-active`,payload:()=>({icon_id:e.ID,active:!e.Active}),onDone:r})})})]},e.ID)),e.length===0&&(0,W.jsx)(jt,{colSpan:n?7:6})]})]})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Retiring an icon stops it from being granted to anybody new. Marks already carrying it keep it: the icon is copied onto the mark when it is granted.`})]})]})}function Jr({verifiers:e,canManage:t,navigate:n}){let[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(`all`),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(`50`),[d,f]=(0,g.useState)([]),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(!1),[b,x]=(0,g.useState)(``);async function S(e=!1){y(!0),x(``);let t=new URLSearchParams({limit:l});r&&t.set(`verifier_bot_id`,r),a!==`all`&&t.set(`peer_type`,a),s.trim()&&t.set(`q`,s.trim().replace(/^@/,``)),e&&h&&t.set(`before_id`,h);try{let n=await k.customVerifications(t),r=n.rows??[];f(t=>e?[...t,...r]:r),_(n.next_before_id??``),m(!!n.has_more)}catch(e){x(O(e))}finally{y(!1)}}return(0,g.useEffect)(()=>{S(!1)},[]),(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Granted marks`,text:`Every peer currently carrying a third-party mark, whoever granted it — an operator decision, the verifier bot itself, or the peer's owner through bots.setCustomVerification.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:v,children:[(0,W.jsx)(Ke,{size:15,className:v?`spin`:``}),` `,`Refresh`]})}),b&&(0,W.jsx)(K,{children:b})]}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),S(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`Peer id, username or title`})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Verifier`}),(0,W.jsx)(Yr,{value:r,verifiers:e,onChange:i})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Peer type`}),(0,W.jsxs)(`select`,{value:a,onChange:e=>o(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All types`}),Vr.map(e=>(0,W.jsx)(`option`,{value:e,children:Ur[e]},e))]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsx)(`input`,{className:`small-input`,value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:v,children:[v?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,`Search`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Verifier`}),(0,W.jsx)(`th`,{children:`Peer`}),(0,W.jsx)(`th`,{children:`Description`}),(0,W.jsx)(`th`,{children:`Icon`}),(0,W.jsx)(`th`,{children:`Filed`}),t&&(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[d.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsxs)(`td`,{className:`mono`,children:[`#`,e.ID]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{children:e.CompanyName||H(e.VerifierBotUsername)||e.VerifierBotID}),(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:H(e.VerifierBotUsername)||e.VerifierBotID})]}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`button`,{className:`row-link`,type:`button`,onClick:()=>n(ei(e.PeerType,e.PeerID)),children:(0,W.jsx)(`strong`,{children:$r(e)})}),(0,W.jsxs)(`div`,{className:`entity-subtitle mono`,children:[Ur[e.PeerType],` · `,e.PeerID]})]}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Description||`Not set`}),(0,W.jsx)(`td`,{className:`mono`,children:e.IconDocumentID}),(0,W.jsx)(`td`,{children:U(e.CreatedAt)||`-`}),t&&(0,W.jsx)(`td`,{children:(0,W.jsx)(`div`,{className:`row-actions`,children:(0,W.jsx)(Z,{label:`Remove mark`,icon:(0,W.jsx)(de,{size:14}),tone:`danger`,compact:!0,path:`/api/actions/revoke-custom-verification`,payload:()=>({verifier_bot_id:e.VerifierBotID,peer_type:e.PeerType,peer_id:e.PeerID}),onDone:()=>S(!1)})})})]},e.ID)),d.length===0&&(0,W.jsx)(jt,{colSpan:t?7:6})]})]})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Removing a mark clears the icon and the description from the peer. The application it came from keeps its history.`}),p&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!0),disabled:v,children:[v?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(he,{size:15}),` `,`Load more`]})})]})}function Yr({value:e,verifiers:t,onChange:n}){return(0,W.jsxs)(`select`,{value:e,onChange:e=>n(e.target.value),children:[(0,W.jsx)(`option`,{value:``,children:`All verifiers`}),t.map(e=>(0,W.jsx)(`option`,{value:e.BotID,children:`${e.CompanyName||e.BotID} · ${H(e.BotUsername)||e.BotID}`+(e.Enabled?``:` (disabled)`)},e.BotID))]})}function Xr({status:e}){return(0,W.jsx)(q,{tone:Zr(e),children:Hr[e]})}function Zr(e){return e===`approved`?`good`:e===`pending`?`warn`:e===`rejected`?`danger`:`neutral`}function Qr(e,t){return e===`pending`?t!==`0`&&t!==``?`warn`:`neutral`:e===`approved`?`good`:`neutral`}function $r(e){return H(e.PeerUsername)||e.PeerTitle||`#${e.PeerID}`}function ei(e,t){return e===`channel`?`/channels/${t}`:`/accounts/${t}`}function ti({id:e,navigate:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);try{r(await k.customVerificationRequest(e))}catch(e){d(O(e))}finally{l(!1)}}function p(){s(!1),f()}(0,g.useEffect)(()=>{f()},[e]);function m(e){if(e instanceof v&&e.status===409)return s(!0),f(),`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}if(u&&!n)return(0,W.jsx)(K,{children:u});if(!n)return(0,W.jsx)(X,{label:`Loading the application…`});let h=n.request,_=ri(n.verifier),y=n.mark_active,b=h.Status===`pending`,x=h.Status===`approved`,S=i.trim(),C=h.RequestedDescription.trim(),w=!!_?.CanModifyCustomDescription&&C!==``,T=w?C:(_?.DefaultDescription??``).trim();function E(){let e={version:h.Version};return S&&(e.internal_note=S),e}function D(){a(``),s(!1),f()}return(0,W.jsxs)(Dt,{title:`Application #${h.ID}`,eyebrow:`Third-party verification / Review`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/bot-verification`),children:[(0,W.jsx)(le,{size:15}),` `,`Back to list`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:p,disabled:c,children:[(0,W.jsx)(Ke,{size:15,className:c?`spin`:``}),` `,`Refresh`]})]}),children:[u&&(0,W.jsx)(K,{children:u}),o&&(0,W.jsx)(K,{children:`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}),(0,W.jsx)(kt,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:$r(h)}),(0,W.jsxs)(`div`,{className:`entity-subtitle mono`,children:[`#`,h.ID,` · `,Ur[h.PeerType],`:`,h.PeerID,` · v`,h.Version]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(Xr,{status:h.Status}),y?(0,W.jsxs)(q,{tone:`good`,children:[(0,W.jsx)(F,{size:12}),` `,`Mark is live`]}):(0,W.jsx)(q,{tone:`neutral`,children:`No mark on the peer`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`A verifier company's icon — not the official checkmark`,text:`A third-party mark is a verifier bot's own icon, drawn right BEFORE the name of an account, a bot or a channel, plus one line of description in the profile. It says “this verifier vouches for this peer”, and nothing more.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The icon is a custom emoji document. The client fetches it through messages.getCustomEmojiDocuments, so a document id that resolves to nothing renders as no badge at all — which is why marks are granted from the catalogue below rather than from a typed number.`})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Verifier`,text:`The company whose icon the peer would carry, as its row stands right now.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/bots/${h.VerifierBotID}`),children:[(0,W.jsx)(fe,{size:15}),` `,`Open verifier bot`]})}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Company`,value:_?.CompanyName||`-`}),(0,W.jsx)(Y,{label:`Bot`,value:H(h.VerifierBotUsername)||`-`}),(0,W.jsx)(Y,{label:`Verifier bot ID`,value:h.VerifierBotID,mono:!0}),(0,W.jsx)(Y,{label:`Document ID`,value:_?.IconDocumentID||`-`,mono:!0}),(0,W.jsx)(Y,{label:`Name`,value:_?.IconName||`-`}),(0,W.jsx)(Y,{label:`Own description`,value:_?.CanModifyCustomDescription?`Yes`:`No`})]}),(0,W.jsx)(ni,{label:`Default description`,children:_?.DefaultDescription?(0,W.jsx)(`p`,{className:`about-text`,children:_.DefaultDescription}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),!_&&(0,W.jsx)(K,{children:`The verifier row is gone: its status was revoked after this application was filed. There is no icon to grant, so the application can only be rejected.`}),_&&!_.Enabled&&(0,W.jsx)(K,{children:`This verifier is disabled. It cannot mark anything new until an operator enables it again.`})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Peer`,text:`The account, bot or channel the icon would be attached to.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(ei(h.PeerType,h.PeerID)),children:[(0,W.jsx)(xe,{size:15}),` `,`Open peer`]})}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Type`,value:Ur[h.PeerType]}),(0,W.jsx)(Y,{label:`Username`,value:H(h.PeerUsername)||`-`}),(0,W.jsx)(Y,{label:`Title`,value:h.PeerTitle||`-`}),(0,W.jsx)(Y,{label:`Peer ID`,value:h.PeerID,mono:!0})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Applicant`,text:`Who filed the application with the verifier bot.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/accounts/${h.ApplicantUserID}`),children:[(0,W.jsx)(st,{size:15}),` `,`Open account`]})}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Username`,value:H(h.ApplicantUsername)||`-`}),(0,W.jsx)(Y,{label:`User ID`,value:h.ApplicantUserID,mono:!0}),(0,W.jsx)(Y,{label:`Filed`,value:U(h.CreatedAt)||`-`}),(0,W.jsx)(Y,{label:`Updated`,value:U(h.UpdatedAt)||`-`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Application`,text:`What the applicant wrote, rendered as plain text.`}),(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Correlation ID`,value:h.CorrelationID||`-`,mono:!0}),(0,W.jsx)(Y,{label:`Status`,value:Hr[h.Status]})]}),(0,W.jsx)(ni,{label:`Stated reason`,children:h.Reason?(0,W.jsx)(`p`,{className:`about-text`,children:h.Reason}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),(0,W.jsx)(ni,{label:`Requested description`,children:C?(0,W.jsx)(`p`,{className:`about-text`,children:C}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),(0,W.jsx)(ni,{label:`Description the mark would carry`,children:T?(0,W.jsx)(`p`,{className:`about-text`,children:T}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Resolved the same way the backend resolves it: the applicant's wording only when this verifier may set its own description, otherwise the verifier's default.`}),C!==``&&!w&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This verifier may not set a per-peer description, so the requested wording is ignored and the default is applied.`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Decision`,text:`What was decided, by whom, and with which wording.`}),(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Decided by`,value:h.DecidedBy||`-`}),(0,W.jsx)(Y,{label:`Approved`,value:U(h.ApprovedAt)||`-`}),(0,W.jsx)(Y,{label:`Rejected`,value:U(h.RejectedAt)||`-`}),(0,W.jsx)(Y,{label:`Version (optimistic lock)`,value:h.Version,mono:!0})]}),(0,W.jsx)(ni,{label:`Decision reason`,children:h.DecisionReason?(0,W.jsx)(`p`,{className:`about-text`,children:h.DecisionReason}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`No decision yet`})}),(0,W.jsx)(ni,{label:`Internal note · admins only`,children:h.InternalNote?(0,W.jsx)(`p`,{className:`about-text`,children:h.InternalNote}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not set`})})]})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsxs)(`div`,{className:`dock-title`,children:[(0,W.jsx)(tt,{size:14}),` `,`Decision`]}),!b&&!x&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This status has no available actions.`}),(b||x)&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Internal note`}),(0,W.jsx)(`textarea`,{value:i,onChange:e=>a(e.target.value),rows:3,placeholder:`Handover note for other admins`})]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Optional. Stored with the decision and visible to admins only — never sent to the applicant.`})]}),b&&(0,W.jsxs)(W.Fragment,{children:[!_&&(0,W.jsx)(K,{children:`The verifier row is gone: its status was revoked after this application was filed. There is no icon to grant, so the application can only be rejected.`}),_&&!_.Enabled&&(0,W.jsx)(K,{children:`This verifier is disabled. It cannot mark anything new until an operator enables it again.`}),y&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This peer already carries this verifier's mark; approving refreshes the description and records the decision.`}),(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:`Approve`,icon:(0,W.jsx)(I,{size:15}),tone:`neutral`,path:`/api/botverification/requests/${h.ID}/approve`,payload:E,onDone:D,onError:m}),(0,W.jsx)(Z,{label:`Reject`,icon:(0,W.jsx)(te,{size:15}),tone:`warn`,path:`/api/botverification/requests/${h.ID}/reject`,payload:E,onDone:D,onError:m})]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Puts the verifier's icon before the peer's name and its description in the profile, and messages the applicant.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The reason is mandatory: it is the wording the applicant is told, so write what exactly was missing.`})]}),x&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`div`,{className:`dock-title`,children:[(0,W.jsx)(Qe,{size:14}),` `,`Danger zone`]}),(0,W.jsxs)(`div`,{className:`danger-zone`,children:[(0,W.jsx)(Z,{label:`Revoke mark`,icon:(0,W.jsx)(de,{size:15}),tone:`danger`,path:`/api/botverification/requests/${h.ID}/revoke`,payload:E,onDone:D,onError:m}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Takes the icon and the description off the peer and closes the application as revoked. The official checkmark, if the peer has one, is untouched.`}),!y&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The peer carries no mark right now — revoking only closes the application.`})]})]})]})})]})}function ni({label:e,children:t}){return(0,W.jsxs)(`div`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:e}),t]})}function ri(e){return!e||!e.BotID||e.BotID===`0`?null:e}var ii=[`draft`,`submitted`,`in_review`,`approved`,`rejected`,`cancelled`],ai=[`bot`,`channel`,`supergroup`,`user`],oi={draft:`Draft`,submitted:`Submitted`,in_review:`In review`,approved:`Approved`,rejected:`Rejected`,cancelled:`Cancelled`},si={bot:`Bot`,channel:`Channel`,supergroup:`Supergroup`,user:`User`};function ci({navigate:e}){let[t,n]=(0,g.useState)(`all`),[r,i]=(0,g.useState)(`all`),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(`50`),[d,f]=(0,g.useState)([]),[p,m]=(0,g.useState)({}),[h,_]=(0,g.useState)(!1),[v,y]=(0,g.useState)(``),[b,x]=(0,g.useState)(!1),[S,C]=(0,g.useState)(``);async function w(e=!1){x(!0),C(``);let n=new URLSearchParams({limit:l});t!==`all`&&n.set(`status`,t),r!==`all`&&n.set(`target_type`,r),a.trim()&&n.set(`reviewer`,a.trim()),s.trim()&&n.set(`q`,s.trim().replace(/^@/,``)),e&&v&&n.set(`before_id`,v);try{let t=await k.verificationApplications(n),r=t.rows??[];f(t=>e?[...t,...r]:r),y(t.next_before_id??``),_(!!t.has_more)}catch(e){C(O(e))}finally{x(!1)}}async function T(){try{m((await k.verificationCounts()).counts??{})}catch(e){C(O(e))}}(0,g.useEffect)(()=>{w(!1),T()},[]);function E(){w(!1),T()}return(0,W.jsxs)(Dt,{title:`Verification queue`,eyebrow:`Verification / Queue`,actions:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:E,disabled:b,children:[(0,W.jsx)(Ke,{size:15,className:b?`spin`:``}),` `,`Refresh`]}),children:[S&&(0,W.jsx)(K,{children:S}),(0,W.jsx)(`div`,{className:`metric-row`,children:ii.map(e=>(0,W.jsx)(J,{label:oi[e],value:p[e]??`0`,mono:!0,tone:di(e,p[e]??`0`)},e))}),(0,W.jsx)(Ot,{children:(0,W.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),w(!1)},children:[(0,W.jsxs)(`label`,{className:`searchbox`,children:[(0,W.jsx)(V,{size:15}),(0,W.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:`Application id, peer id, username or title`})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Status`}),(0,W.jsxs)(`select`,{value:t,onChange:e=>n(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All statuses`}),ii.map(e=>(0,W.jsx)(`option`,{value:e,children:oi[e]},e))]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Target type`}),(0,W.jsxs)(`select`,{value:r,onChange:e=>i(e.target.value),children:[(0,W.jsx)(`option`,{value:`all`,children:`All types`}),ai.map(e=>(0,W.jsx)(`option`,{value:e,children:si[e]},e))]})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Reviewer`}),(0,W.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`Any reviewer`})]}),(0,W.jsxs)(`label`,{className:`field-inline`,children:[(0,W.jsx)(`span`,{children:`Limit`}),(0,W.jsx)(`input`,{className:`small-input`,value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,W.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:b,children:[b?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(V,{size:15}),` `,`Search`]})]})}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`ID`}),(0,W.jsx)(`th`,{children:`Target`}),(0,W.jsx)(`th`,{children:`Applicant`}),(0,W.jsx)(`th`,{children:`Category`}),(0,W.jsx)(`th`,{children:`Status`}),(0,W.jsx)(`th`,{children:`Submitted`}),(0,W.jsx)(`th`,{children:`Reviewer`}),(0,W.jsx)(`th`,{})]})}),(0,W.jsxs)(`tbody`,{children:[d.map(t=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{className:`mono`,children:(0,W.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/verification/${t.ID}`),children:[`#`,t.ID]})}),(0,W.jsxs)(`td`,{children:[(0,W.jsx)(`strong`,{children:fi(t)}),(0,W.jsxs)(`div`,{className:`entity-subtitle mono`,children:[si[t.TargetType],` · `,t.TargetID]}),t.TargetVerified&&(0,W.jsxs)(q,{tone:`good`,children:[(0,W.jsx)(F,{size:12}),` `,`Badge already on`]})]}),(0,W.jsxs)(`td`,{children:[H(t.ApplicantUsername)||t.ApplicantName||`-`,(0,W.jsx)(`div`,{className:`entity-subtitle mono`,children:t.ApplicantUserID})]}),(0,W.jsx)(`td`,{children:t.Category||`-`}),(0,W.jsx)(`td`,{children:(0,W.jsx)(li,{status:t.Status})}),(0,W.jsx)(`td`,{children:U(t.SubmittedAt)||`-`}),(0,W.jsx)(`td`,{children:t.ReviewerAdminID||`-`}),(0,W.jsx)(`td`,{children:(0,W.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/verification/${t.ID}`),children:[(0,W.jsx)(Ze,{size:14}),` `,`Details`,` `,(0,W.jsx)(_e,{size:14})]})})]},t.ID)),d.length===0&&(0,W.jsx)(jt,{colSpan:8})]})]})}),h&&(0,W.jsx)(`div`,{className:`toolbar`,children:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>w(!0),disabled:b,children:[b?(0,W.jsx)(L,{size:15,className:`spin`}):(0,W.jsx)(he,{size:15}),` `,`Load more`]})})]})}function li({status:e}){return(0,W.jsx)(q,{tone:ui(e),children:oi[e]})}function ui(e){return e===`approved`?`good`:e===`submitted`||e===`in_review`?`warn`:e===`rejected`?`danger`:`neutral`}function di(e,t){return e===`submitted`||e===`in_review`?t!==`0`&&t!==``?`warn`:`neutral`:e===`approved`?`good`:`neutral`}function fi(e){return H(e.TargetUsername)||e.TargetTitle||`#${e.TargetID}`}function pi(e){return e.TargetType===`bot`?`/bots/${e.TargetID}`:e.TargetType===`user`?`/accounts/${e.TargetID}`:`/channels/${e.TargetID}`}var mi={created:`Created`,updated:`Updated`,submitted:`Submitted`,claimed:`Claimed`,approved:`Approved`,rejected:`Rejected`,cancelled:`Cancelled`,revoked:`Badge revoked`,notified:`Applicant notified`};function hi({id:e,navigate:t}){let{can:n}=zt(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(!1),[d,f]=(0,g.useState)(``);async function p(){u(!0),f(``);try{i(await k.verificationApplication(e))}catch(e){f(O(e))}finally{u(!1)}}function m(){c(!1),p()}(0,g.useEffect)(()=>{p()},[e]);function h(e){if(e instanceof v&&e.status===409)return c(!0),p(),`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}if(d&&!r)return(0,W.jsx)(K,{children:d});if(!r)return(0,W.jsx)(X,{label:`Loading the application…`});let _=r.application,y=r.events??[],b=r.applicant_controls_target,x=r.target_verified,S=_.Status===`submitted`,C=_.Status===`submitted`||_.Status===`in_review`,w=_.Status===`approved`&&n(`verification.revoke`),T=a.trim();function E(){let e={version:_.Version};return T&&(e.internal_note=T),e}function D(){o(``),c(!1),p()}return(0,W.jsxs)(Dt,{title:`Application #${_.ID}`,eyebrow:`Verification / Review`,actions:(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/verification`),children:[(0,W.jsx)(le,{size:15}),` `,`Back to list`]}),(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:m,disabled:l,children:[(0,W.jsx)(Ke,{size:15,className:l?`spin`:``}),` `,`Refresh`]})]}),children:[d&&(0,W.jsx)(K,{children:d}),s&&(0,W.jsx)(K,{children:`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`}),(0,W.jsx)(kt,{main:(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`section`,{className:`entity-head`,children:[(0,W.jsxs)(`div`,{children:[(0,W.jsx)(`div`,{className:`entity-title`,children:fi(_)}),(0,W.jsxs)(`div`,{className:`entity-subtitle mono`,children:[`#`,_.ID,` · `,si[_.TargetType],`:`,_.TargetID,` · v`,_.Version]})]}),(0,W.jsxs)(`div`,{className:`entity-badges`,children:[(0,W.jsx)(li,{status:_.Status}),x&&(0,W.jsxs)(q,{tone:`good`,children:[(0,W.jsx)(F,{size:12}),` `,`Badge already on`]}),(0,W.jsx)(q,{tone:b?`good`:`danger`,children:b?`Control confirmed`:`No control over the target`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Target`,text:`The peer the badge would be attached to, as it exists right now.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(pi(_)),children:[(0,W.jsx)(xe,{size:15}),` `,`Open target`]})}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Type`,value:si[_.TargetType]}),(0,W.jsx)(Y,{label:`Username`,value:H(_.TargetUsername)||`-`}),(0,W.jsx)(Y,{label:`Title`,value:_.TargetTitle||`-`}),(0,W.jsx)(Y,{label:`Peer ID`,value:_.TargetID,mono:!0})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Applicant`,text:`Who filed the application and whether they still hold rights on the target.`,action:(0,W.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/accounts/${_.ApplicantUserID}`),children:[(0,W.jsx)(st,{size:15}),` `,`Open account`]})}),(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Username`,value:H(_.ApplicantUsername)||`-`}),(0,W.jsx)(Y,{label:`Name`,value:_.ApplicantName||`-`}),(0,W.jsx)(Y,{label:`User ID`,value:_.ApplicantUserID,mono:!0}),(0,W.jsx)(Y,{label:`Submitted`,value:U(_.SubmittedAt)||`-`})]}),b?(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The applicant controls the target right now — checked against the live records, not against the submission snapshot.`}):(0,W.jsx)(K,{children:`The applicant no longer controls the target. Approving would hand the badge to someone who does not hold the peer — normally a reason to reject.`})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Application`,text:`Everything the applicant submitted, rendered as plain text.`}),(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Category`,value:_.Category||`-`}),(0,W.jsx)(Y,{label:`Correlation ID`,value:_.CorrelationID||`-`,mono:!0}),(0,W.jsx)(Y,{label:`Created`,value:U(_.CreatedAt)||`-`}),(0,W.jsx)(Y,{label:`Updated`,value:U(_.UpdatedAt)||`-`})]}),(0,W.jsx)(gi,{label:`Description`,children:_.Description?(0,W.jsx)(`p`,{className:`about-text`,children:_.Description}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})}),(0,W.jsx)(gi,{label:`Official website`,children:_.OfficialWebsite?(0,W.jsx)(`div`,{className:`about-text`,children:(0,W.jsx)(_i,{value:_.OfficialWebsite})}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})}),(0,W.jsx)(gi,{label:`Social links`,children:(0,W.jsx)(vi,{values:_.SocialLinks})}),(0,W.jsx)(gi,{label:`Press coverage`,children:(0,W.jsx)(vi,{values:_.PressLinks})}),(0,W.jsx)(gi,{label:`Applicant comment`,children:_.AdditionalNote?(0,W.jsx)(`p`,{className:`about-text`,children:_.AdditionalNote}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Only http:// and https:// links are clickable and open in a new tab; anything else is shown as text.`})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`Decision`,text:`What was decided, by whom, and with which wording.`}),(0,W.jsxs)(`div`,{className:`stacked-sections`,children:[(0,W.jsxs)(`div`,{className:`summary-grid`,children:[(0,W.jsx)(Y,{label:`Reviewer`,value:_.ReviewerAdminID||`-`}),(0,W.jsx)(Y,{label:`Decided`,value:U(_.ReviewedAt)||`-`}),(0,W.jsx)(Y,{label:`Status`,value:oi[_.Status]}),(0,W.jsx)(Y,{label:`Version (optimistic lock)`,value:_.Version,mono:!0})]}),(0,W.jsx)(gi,{label:`Decision reason`,children:_.DecisionReason?(0,W.jsx)(`p`,{className:`about-text`,children:_.DecisionReason}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`No decision yet`})}),(0,W.jsx)(gi,{label:`Internal note · admins only`,children:_.InternalNote?(0,W.jsx)(`p`,{className:`about-text`,children:_.InternalNote}):(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`})})]})]}),(0,W.jsxs)(`section`,{className:`section-block`,children:[(0,W.jsx)(G,{title:`History`,text:`Immutable trail of every status transition, with actor and reason.`}),(0,W.jsx)(`div`,{className:`table-wrap`,children:(0,W.jsxs)(`table`,{className:`data-table`,children:[(0,W.jsx)(`thead`,{children:(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`th`,{children:`Event`}),(0,W.jsx)(`th`,{children:`From → to`}),(0,W.jsx)(`th`,{children:`Actor`}),(0,W.jsx)(`th`,{children:`Reason`}),(0,W.jsx)(`th`,{children:`Internal note`}),(0,W.jsx)(`th`,{children:`Time`})]})}),(0,W.jsxs)(`tbody`,{children:[y.map(e=>(0,W.jsxs)(`tr`,{children:[(0,W.jsx)(`td`,{children:(0,W.jsx)(yi,{kind:e.Kind})}),(0,W.jsxs)(`td`,{className:`mono`,children:[e.FromStatus||`-`,` → `,e.ToStatus||`-`]}),(0,W.jsx)(`td`,{children:e.Actor||`-`}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Reason||`-`}),(0,W.jsx)(`td`,{className:`truncate`,children:e.Note||`-`}),(0,W.jsx)(`td`,{children:U(e.CreatedAt)||`-`})]},e.ID)),y.length===0&&(0,W.jsx)(jt,{colSpan:6})]})]})})]})]}),side:(0,W.jsxs)(`section`,{className:`action-dock`,children:[(0,W.jsx)(`div`,{className:`dock-title`,children:`Review actions`}),!S&&!C&&!w&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`This status has no available actions.`}),S&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsx)(`div`,{className:`action-stack`,children:(0,W.jsx)(Z,{label:`Take into review`,icon:(0,W.jsx)(Ee,{size:15}),tone:`neutral`,path:`/api/verification/applications/${_.ID}/claim`,payload:()=>({version:_.Version}),onDone:D,onError:h})}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Assigns the application to you and moves it to in review, so two reviewers never work on the same one.`})]}),(C||w)&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`label`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:`Internal note`}),(0,W.jsx)(`textarea`,{value:a,onChange:e=>o(e.target.value),rows:3,placeholder:`Handover note for other reviewers`})]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Optional. Stored with the decision and visible to admins only — never sent to the applicant.`})]}),C&&(0,W.jsxs)(W.Fragment,{children:[!b&&(0,W.jsx)(K,{children:`The applicant no longer controls the target. Approving would hand the badge to someone who does not hold the peer — normally a reason to reject.`}),x&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The target already carries the badge; approving only records the decision.`}),(0,W.jsxs)(`div`,{className:`action-stack`,children:[(0,W.jsx)(Z,{label:`Approve`,icon:(0,W.jsx)(I,{size:15}),tone:`neutral`,path:`/api/verification/applications/${_.ID}/approve`,payload:E,onDone:D,onError:h}),(0,W.jsx)(Z,{label:`Reject`,icon:(0,W.jsx)(te,{size:15}),tone:`warn`,path:`/api/verification/applications/${_.ID}/reject`,payload:E,onDone:D,onError:h})]}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Grants the official badge to the target and closes the application.`}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The reason is mandatory: it is the wording the applicant is told, so write what exactly was missing.`})]}),w&&(0,W.jsxs)(W.Fragment,{children:[(0,W.jsxs)(`div`,{className:`dock-title`,children:[(0,W.jsx)(Qe,{size:14}),` `,`Danger zone`]}),(0,W.jsxs)(`div`,{className:`danger-zone`,children:[(0,W.jsx)(Z,{label:`Revoke verification`,icon:(0,W.jsx)(de,{size:15}),tone:`danger`,path:`/api/actions/revoke-verification`,payload:()=>{let e={target_type:_.TargetType,target_id:_.TargetID};return T&&(e.internal_note=T),e},onDone:D,onError:h}),(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Clears the badge from the target. The approved application stays in history.`}),!x&&(0,W.jsx)(`p`,{className:`bot-create-note`,children:`The target carries no badge right now — there is nothing to revoke.`})]})]})]})})]})}function gi({label:e,children:t}){return(0,W.jsxs)(`div`,{className:`duration-field`,children:[(0,W.jsx)(`span`,{children:e}),t]})}function _i({value:e}){let t=ht(e);return t?(0,W.jsxs)(`a`,{className:`row-link`,href:t,target:`_blank`,rel:`noopener noreferrer`,children:[e,` `,(0,W.jsx)(xe,{size:13})]}):(0,W.jsx)(`span`,{className:`mono`,children:e})}function vi({values:e}){let t=(e??[]).filter(e=>e.trim()!==``);return t.length===0?(0,W.jsx)(`p`,{className:`bot-create-note`,children:`Not provided`}):(0,W.jsx)(`div`,{className:`about-text`,children:t.map((e,t)=>(0,W.jsx)(`div`,{children:(0,W.jsx)(_i,{value:e})},`${t}-${e}`))})}function yi({kind:e}){return(0,W.jsx)(q,{tone:e===`approved`?`good`:e===`rejected`||e===`revoked`||e===`cancelled`?`danger`:e===`submitted`||e===`claimed`?`warn`:`neutral`,children:mi[e]})}function bi({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1],i=e.path.match(/^\/bots\/(\d+)$/)?.[1],a=e.path.match(/^\/moderation\/(\d+)$/)?.[1],o=e.path.match(/^\/collectible-usernames\/(\d+)$/)?.[1],s=e.path.match(/^\/verification\/(\d+)$/)?.[1],c=e.path.match(/^\/bot-verification\/(\d+)$/)?.[1];return c?(0,W.jsx)(Wt,{children:(0,W.jsx)(Ht,{permission:Ft,children:(0,W.jsx)(ti,{id:c,navigate:t})})}):e.path===`/bot-verification`?(0,W.jsx)(Wt,{children:(0,W.jsx)(Ht,{permission:Ft,children:(0,W.jsx)(Wr,{navigate:t})})}):s?(0,W.jsx)(Ht,{permission:Pt,children:(0,W.jsx)(hi,{id:s,navigate:t})}):e.path===`/verification`?(0,W.jsx)(Ht,{permission:Pt,children:(0,W.jsx)(ci,{navigate:t})}):o?(0,W.jsx)(Bn,{id:o,navigate:t}):e.path===`/collectible-usernames`?(0,W.jsx)(In,{navigate:t}):e.path===`/reserved-usernames`?(0,W.jsx)(Wn,{}):e.path===`/storage`?(0,W.jsx)(Fr,{navigate:t}):n?(0,W.jsx)(wn,{id:Number(n),navigate:t}):r?(0,W.jsx)(Kn,{id:Number(r),navigate:t}):i?(0,W.jsx)(Xn,{id:Number(i),navigate:t}):a?(0,W.jsx)(Ar,{id:Number(a),navigate:t}):e.path===`/accounts/shared-devices`?(0,W.jsx)(An,{navigate:t}):e.path===`/accounts`?(0,W.jsx)(kn,{navigate:t}):e.path===`/channels`?(0,W.jsx)(Jn,{navigate:t}):e.path===`/bots`?(0,W.jsx)($n,{navigate:t}):e.path===`/moderation`?(0,W.jsx)(Cr,{navigate:t}):e.path===`/broadcasts`?(0,W.jsx)(nr,{}):e.path===`/emoji`?(0,W.jsx)(_r,{kind:`emoji`}):e.path===`/stickers`?(0,W.jsx)(_r,{kind:`stickers`}):e.path===`/gif-catalog`?(0,W.jsx)(yr,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,W.jsx)(lr,{ownerUserID:Number(e.search.get(`owner_user_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups/detail`?(0,W.jsx)(sr,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups`?(0,W.jsx)(cr,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,W.jsx)(ur,{navigate:t}):(0,W.jsx)(rr,{navigate:t})}function xi(){let[e,t]=(0,g.useState)(void 0),[n,r]=(0,g.useState)(()=>Gt());(0,g.useEffect)(()=>{let e=()=>r(Gt());return window.addEventListener(`popstate`,e),()=>window.removeEventListener(`popstate`,e)},[]),(0,g.useEffect)(()=>{k.session().then(e=>t(e)).catch(()=>t(null))},[]);let i=e=>{window.history.pushState(null,``,e),r(Gt())};return e===void 0?(0,W.jsx)(tn,{}):e===null?(0,W.jsx)(an,{onLogin:t}):(0,W.jsx)(Rt,{permissions:e.permissions??[],hideThirdPartyVerification:e.hide_third_party_verification??!0,children:(0,W.jsx)(nn,{actor:e.actor,route:n,navigate:i,onLogout:()=>t(null),children:(0,W.jsx)(bi,{route:n,navigate:i})})})}_.createRoot(document.getElementById(`root`)).render((0,W.jsx)(g.StrictMode,{children:(0,W.jsx)(Xt,{children:(0,W.jsx)(xi,{})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/index.html b/cmd/telesrv-admin/web/dist/index.html index 8256b328..9132c692 100644 --- a/cmd/telesrv-admin/web/dist/index.html +++ b/cmd/telesrv-admin/web/dist/index.html @@ -1,32 +1,32 @@ - - - - - - - OwpenGram Admin - - - + + + + + + + OwpenGram Admin + + + - - -
- - + + +
+ + diff --git a/cmd/telesrv-admin/web/src/api.ts b/cmd/telesrv-admin/web/src/api.ts index cd15ada6..c0cfc777 100644 --- a/cmd/telesrv-admin/web/src/api.ts +++ b/cmd/telesrv-admin/web/src/api.ts @@ -22,6 +22,7 @@ import type { ChannelListResponse, CollectibleUsernameDetail, CollectibleUsernameListResponse, + ReservedUsernameListResponse, CommandResult, GroupMessageDetail, GroupMessageListResponse, @@ -163,6 +164,8 @@ export const api = { request(`/api/collectible-usernames?${params.toString()}`), collectibleUsername: (id: string) => request(`/api/collectible-usernames/${encodeURIComponent(id)}`), + reservedUsernames: (params: URLSearchParams) => + request(`/api/reserved-usernames?${params.toString()}`), dashboard: () => request("/api/dashboard"), storageStats: () => request("/api/storage/stats"), storageAccounts: (params: URLSearchParams) => diff --git a/cmd/telesrv-admin/web/src/components/Layout.tsx b/cmd/telesrv-admin/web/src/components/Layout.tsx index bb9dea00..e45d2897 100644 --- a/cmd/telesrv-admin/web/src/components/Layout.tsx +++ b/cmd/telesrv-admin/web/src/components/Layout.tsx @@ -1,6 +1,7 @@ import { AtSign, BadgeCheck, + Ban, Bot, ChevronDown, Database, @@ -99,6 +100,7 @@ export function Shell({ } href="/bot-verification" route={route} navigate={navigate}>{"Third-party marks"} )} } href="/collectible-usernames" route={route} navigate={navigate}>{"NFT Usernames"} + } href="/reserved-usernames" route={route} navigate={navigate}>{"Reserved Usernames"} } href="/storage" route={route} navigate={navigate}>{"Storage"} } href="/stickers" route={route} navigate={navigate}>{"Stickers"} } href="/emoji" route={route} navigate={navigate}>{"Emoji"} diff --git a/cmd/telesrv-admin/web/src/pages/ReservedUsernamesPage.tsx b/cmd/telesrv-admin/web/src/pages/ReservedUsernamesPage.tsx new file mode 100644 index 00000000..f7308ba4 --- /dev/null +++ b/cmd/telesrv-admin/web/src/pages/ReservedUsernamesPage.tsx @@ -0,0 +1,176 @@ +import { Loader2, Plus, RefreshCw, Search, Trash2, X } from "lucide-react"; +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { api, errorMessage } from "../api"; +import { ActionButton } from "../components/ActionButton"; +import { Alert, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui"; +import { formatUnix } from "../lib/format"; +import type { ReservedUsernameRow } from "../types"; + +// Reserved usernames are a plain operator blocklist: a name listed here cannot be +// taken as an editable username by any peer and cannot be minted as a +// collectible. No owner, no price, no "bought on Fragment" badge - that is the +// collectible tab's job. +export function ReservedUsernamesPage() { + const [q, setQ] = useState(""); + const [reserveOpen, setReserveOpen] = useState(false); + const [rows, setRows] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function load() { + setLoading(true); + setError(""); + const params = new URLSearchParams({ limit: "200" }); + if (q.trim()) params.set("q", q.trim().replace(/^@/, "")); + try { + const result = await api.reservedUsernames(params); + setRows(result.reserved ?? []); + } catch (err) { + setError(errorMessage(err)); + } finally { + setLoading(false); + } + } + + useEffect(() => { + void load(); + }, []); + + return ( + + + + + } + > + {error && {error}} +
+ +
+ + +
{ + event.preventDefault(); + void load(); + }} + > + + +
+
+ +
+ + + + + + + + + + + + {rows.map((row) => ( + + + + + + + + ))} + {rows.length === 0 && } + +
{"Username"}{"Reason"}{"Reserved by"}{"Reserved (UTC)"}
{`@${row.username}`}{row.reason || "-"}{row.actor || "-"}{formatUnix(row.created_at) || "-"} + } + tone="danger" + path="/api/actions/unreserve-username" + payload={() => ({ username: row.username })} + onDone={() => void load()} + /> +
+
+ + {reserveOpen && ( + setReserveOpen(false)} + onDone={() => { + setReserveOpen(false); + void load(); + }} + /> + )} +
+ ); +} + +// ReserveUsernameModal collects the name, then hands off to ActionButton for the +// standard reason / dry-run / confirm flow - the same as every other admin +// action. The name is read fresh from state on each ActionButton render. +function ReserveUsernameModal({ onClose, onDone }: { onClose: () => void; onDone: () => void }) { + const [username, setUsername] = useState(""); + const clean = username.trim().replace(/^@/, ""); + + return createPortal( +
+
+
+
+
{"Usernames"}
+

{"Reserve a username"}

+
+ +
+
+ +

+ {`No peer will be able to take @${clean || "…"} until it is unreserved. Nothing is shown to users.`} +

+
+
+ + } + tone="neutral" + path="/api/actions/reserve-username" + payload={() => ({ username: clean })} + onDone={onDone} + /> +
+
+
, + document.body, + ); +} diff --git a/cmd/telesrv-admin/web/src/pages/Routes.tsx b/cmd/telesrv-admin/web/src/pages/Routes.tsx index 399f24f8..3a225e2e 100644 --- a/cmd/telesrv-admin/web/src/pages/Routes.tsx +++ b/cmd/telesrv-admin/web/src/pages/Routes.tsx @@ -4,6 +4,7 @@ import { AccountsPage } from "./AccountsPage"; import { SharedDevicesPage } from "./SharedDevicesPage"; import { CollectibleUsernameDetailPage } from "./CollectibleUsernameDetailPage"; import { CollectibleUsernamesPage } from "./CollectibleUsernamesPage"; +import { ReservedUsernamesPage } from "./ReservedUsernamesPage"; import { ChannelDetailPage } from "./ChannelDetailPage"; import { ChannelsPage } from "./ChannelsPage"; import { BotDetailPage } from "./BotDetailPage"; @@ -82,6 +83,9 @@ export function Routes({ route, navigate }: { route: RouteState; navigate: Navig if (route.path === "/collectible-usernames") { return ; } + if (route.path === "/reserved-usernames") { + return ; + } if (route.path === "/storage") { return ; } diff --git a/cmd/telesrv-admin/web/src/routing.ts b/cmd/telesrv-admin/web/src/routing.ts index eb1f67fd..7a3b7551 100644 --- a/cmd/telesrv-admin/web/src/routing.ts +++ b/cmd/telesrv-admin/web/src/routing.ts @@ -20,6 +20,7 @@ export function routeTitle(pathname: string): string { if (pathname.startsWith("/bot-verification")) return "Third-party verification"; if (pathname.startsWith("/verification")) return "Official Verification"; if (pathname.startsWith("/collectible-usernames")) return "Collectible Usernames"; + if (pathname.startsWith("/reserved-usernames")) return "Reserved Usernames"; if (pathname.startsWith("/storage")) return "Storage"; if (pathname.startsWith("/accounts/shared-devices")) return "Shared Devices"; if (pathname.startsWith("/accounts")) return "Accounts"; diff --git a/cmd/telesrv-admin/web/src/types.ts b/cmd/telesrv-admin/web/src/types.ts index fdcb5835..36370fba 100644 --- a/cmd/telesrv-admin/web/src/types.ts +++ b/cmd/telesrv-admin/web/src/types.ts @@ -347,6 +347,17 @@ export type CollectibleUsernameDetail = { transfers: CollectibleUsernameTransferRow[] | null; }; +export type ReservedUsernameRow = { + username: string; + reason: string; + actor: string; + created_at: number; +}; + +export type ReservedUsernameListResponse = { + reserved: ReservedUsernameRow[] | null; +}; + // Official platform verification. Every int64 the backend tags `,string` stays a // decimal string here: application ids, peer ids and the optimistic-locking // version all outgrow the exact range of a JSON number, and a rounded version diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index ca78cf4e..e297a8d8 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -1007,13 +1007,16 @@ func run(logger *zap.Logger) error { account.WithLoginEmailVerification(codeStore, loginEmailSender, cfg.AuthCodeTTL, cfg.AuthCodeMaxAttempts, cfg.LoginEmailCodeLength)) } accountService := account.NewService(passwordStore, accountOptions...) + reservedUsernameStore := postgres.NewReservedUsernameStore(pool) botsService := botsapp.NewService(userStore, botStore, messageStore, botsapp.WithLogger(logger.Named("bots")), botsapp.WithBlockChecker(contactStore), botsapp.WithPublicChannelUsernameResolver(channelStore), + botsapp.WithReservedUsernames(reservedUsernameStore), botsapp.WithUserCache(userCache), botsapp.WithStickerSetCreator(filesService), botsapp.WithGifCatalogSource(filesService), + botsapp.WithBotAvatarStore(filesService), botsapp.WithUserStickerSets(accountService), botsapp.WithTelegramLogin(telegramLoginService), botsapp.WithDialogRateLimiter(rateLimiter, cfg.VerificationBotRateLimit, cfg.VerificationBotRateWindow), @@ -1389,6 +1392,7 @@ func run(logger *zap.Logger) error { Emoji: filesService, Moderation: moderationService, Usernames: usernamesService, + ReservedUsernames: reservedUsernameStore, Verification: verificationService, BotVerification: botVerificationService, Account: accountService, diff --git a/deploy/migrations/20260909190000_reserved_usernames.down.sql b/deploy/migrations/20260909190000_reserved_usernames.down.sql new file mode 100644 index 00000000..b3edb27a --- /dev/null +++ b/deploy/migrations/20260909190000_reserved_usernames.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS public.reserved_usernames; diff --git a/deploy/migrations/20260909190000_reserved_usernames.up.sql b/deploy/migrations/20260909190000_reserved_usernames.up.sql new file mode 100644 index 00000000..6e157737 --- /dev/null +++ b/deploy/migrations/20260909190000_reserved_usernames.up.sql @@ -0,0 +1,17 @@ +-- Operator-maintained username blocklist. A name listed here cannot be taken as +-- an editable username by any peer (account.updateUsername, channels.updateUsername, +-- @BotFather /setusername, or the admin set-username actions). It is a plain +-- blocklist: no owner, no price, no Fragment collectible badge. + +CREATE TABLE public.reserved_usernames ( + username_lower text PRIMARY KEY CHECK ( + username_lower <> '' AND username_lower = lower(username_lower) + ), + username text NOT NULL, + reason text NOT NULL DEFAULT '' CHECK (octet_length(reason) <= 512), + actor text NOT NULL DEFAULT '' CHECK (octet_length(actor) <= 256), + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX reserved_usernames_created_at_idx + ON public.reserved_usernames (created_at DESC, username_lower); diff --git a/docs/telegram-feature-comparison.md b/docs/telegram-feature-comparison.md new file mode 100644 index 00000000..174fb19a --- /dev/null +++ b/docs/telegram-feature-comparison.md @@ -0,0 +1,286 @@ +# OwpenGram Server vs. Telegram - Feature Comparison + +This document compares what OwpenGram Server actually implements against the +behaviour of Telegram's official server, as seen through the MTProto API +(layer 228) and the official clients (Telegram Desktop is the primary target, +with Android / iOS / Web compatibility paths). + +It is written from the server code in this repository: ~633 canonical +`registerRPC` handlers across `internal/rpc`, plus the domain services in +`internal/app/*`. "Telegram" below means the closed official backend. + +Legend: + +- **Full** - implemented with real server-side state and semantics +- **Partial** - core paths work; edges, scale features, or moderation depth missing +- **Stub** - RPC is answered with a fixed/empty valid response so clients don't + hang or crash, but there is no feature behind it +- **None** - not registered / not implemented + +--- + +## 1. Transport, auth keys, sessions + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| MTProto 2.0 over TCP | Full | TCP transport, RSA key exchange, auth keys, salts, ack/resend, bad-msg notifications, RPC dispatch | Full | +| Obfuscated / padded intermediate / other transports | Full (TCP, HTTP, WS, obfuscation) | intermediate / padded-intermediate focus | Partial | +| Multiple data centers, CDN DCs, DC migration | Full (5+ DCs, `PHONE_MIGRATE`, CDN file DCs) | Single logical DC; `help.getConfig` advertises one; no CDN redirects | None (by design) | +| Perfect-forward-secrecy temp auth keys (`auth.bindTempAuthKey`) | Full | Implemented | Full | +| Takeout / data export sessions | Full (`account.initTakeoutSession`) | Not registered | None | +| Web / bot authorizations listing & reset | Full | `account.getWebAuthorizations`, `resetWebAuthorization(s)` | Full | + +## 2. Login and accounts + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Phone-number login with SMS/flash-call/app code | Full, global SMS delivery | Dev login code; external delivery via SMS webhook or SMTP; email as identity (no phone required) | Partial (delivery is operator-provided) | +| QR-code login (`auth.exportLoginToken` / `acceptLoginToken`) | Full | Implemented | Full | +| Cloud password / 2FA (SRP) | Full | `account.getPassword`, `updatePasswordSettings`, `auth.checkPassword`, recovery e-mail, `resetPassword` | Full | +| Passkey / WebAuthn sign-in | Not in official server | `auth.initPasskeyLogin` / `finishPasskeyLogin`, `account.*Passkey*` | Extra (OwpenGram-only) | +| "Login with Telegram" as an OIDC provider for 3rd-party sites | Not applicable | Self-hosted OpenID Connect provider (`internal/telegramloginhttp`) | Extra | +| Sign-up, terms of service, delete account, account TTL | Full | Implemented incl. `account.setAccountTTL`, `deleteAccount` with reason | Full | +| Login email as second factor | Full | Implemented | Full | +| Active sessions / authorizations management | Full | `account.getAuthorizations`, `resetAuthorization`, TTL, `changeAuthorizationSettings` | Full | + +## 3. Users, contacts, privacy + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Profiles, bios, profile photos, personal channel | Full | `users.getFullUser`, `photos.*`, `account.updatePersonalChannel` | Full | +| Usernames + collectible/fragment usernames | Full | Mint / transfer / activate, `account.reorderUsernames`, `fragment.getCollectibleInfo` | Full | +| Contact import / export / search / resolve phone | Full | `contacts.importContacts`, `resolvePhone`, `search`, close friends | Full | +| Blocked list, privacy rules, "who can..." keys | Full (all privacy keys) | `account.getPrivacy` / `setPrivacy`, global privacy settings, blocked list | Partial (common keys; some newer keys may be defaulted) | +| Presence / last seen | Full | `account.updateStatus`, `contacts.getStatuses`, presence fan-out | Full | +| Birthdays, close friends, contact notes | Full | `contacts.getBirthdays`, `editCloseFriends`, `updateContactNote` | Full | +| Global name search directory | Full | `contacts.search` over local users/chats | Partial (local instance only) | + +## 4. Messaging (private chats) + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Send / edit / delete / forward / reply | Full | `messages.sendMessage` / `sendMedia` / `sendMultiMedia` / `editMessage` / `forwardMessages` | Full | +| Rich entities, formatted text, link previews | Full | Entities, `messages.getWebPage` / `getWebPagePreview`, TDesktop rich messages | Full | +| Albums / grouped media | Full | `album_group` grouping | Full | +| Reactions (emoji + custom emoji), paid reactions | Full incl. paid (Stars) | `sendReaction`, available/recent/top/default reactions, tags; **no paid reactions** | Partial | +| Scheduled messages | Full | `getScheduledHistory`, `sendScheduledMessages`, `deleteScheduledMessages` | Full | +| Self-destruct / TTL, default history TTL | Full | `setHistoryTTL`, `setDefaultHistoryTTL`, TTL-oriented paths | Partial | +| Read receipts, read date, "who read" in groups | Full | `readHistory`, `getOutboxReadDate`, `getMessageReadParticipants` | Full | +| Drafts (incl. cloud drafts sync) | Full | `saveDraft`, `getAllDrafts`, `clearAllDrafts` | Full | +| Saved Messages, saved dialogs, pinned saved | Full | `getSavedDialogs`, `getSavedHistory`, `toggleSavedDialogPin` | Full | +| Quick replies (business) | Full | `getQuickReplies`, `sendQuickReplyMessages`, `editQuickReplyShortcut` | Full | +| Translation (`messages.translateText`) | Full | Provider-backed batch translation, per-peer language, rate limits | Full | +| Transcribe voice (`messages.transcribeAudio`) | Full (Premium) | Registered | Partial (provider-dependent) | +| Fact-check / sponsored-message plumbing | Full | `getSponsoredMessages`, `viewSponsoredMessage`, `reportSponsoredMessage` | Partial (plumbing) | +| To-do lists in messages | Full | `appendTodoList`, `toggleTodoCompleted` | Full | +| Search: in-chat, global, by date, counters, calendar | Full | `messages.search`, `searchGlobal`, `getSearchCounters`, `getSearchResultsCalendar` / `Positions` | Full | + +## 5. Groups, supergroups, channels + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Basic groups create / add / migrate to supergroup | Full | `messages.createChat`, `addChatUser`, `migrateChat` | Full | +| Supergroups / channels create, join, leave, delete | Full | `channels.createChannel` / `joinChannel` / `leaveChannel` / `deleteChannel` | Full | +| Admin rights, banned rights, default banned rights, ranks | Full | `channels.editAdmin` / `editBanned`, `editChatDefaultBannedRights`, `editChatParticipantRank` | Full | +| Invite links (permanent, named, request-to-join, importers) | Full | `messages.exportChatInvite`, `getExportedChatInvites`, `hideChatJoinRequest`, importers | Full | +| Participants list, admin log, hidden participants | Full | `channels.getParticipants`, `getAdminLog`, `toggleParticipantsHidden` | Full | +| Forum topics | Full | `getForumTopics`, `editForumTopic`, pinned topics, view-as-messages | Full | +| Linked discussion group | Full | `setDiscussionGroup`, `getGroupsForDiscussion`, `readDiscussion` | Full | +| Slow mode, join-to-send, join-request, anti-spam, gigagroup | Full | `toggleSlowMode`, `toggleJoinToSend`, `toggleAntiSpam`, `convertToGigagroup` | Full | +| Public username directory + previews for non-members | Full | `channels.searchPosts`, resolve, public landing pages | Full | +| Boosts / boost level perks | Full | `premium.applyBoost`, `getBoostsStatus`, `getMyBoosts` | Partial (levels tracked; not all perks gated) | +| Channel monetization, paid posts, suggested posts | Full (Stars/TON) | `service_suggested_post`, `toggleSuggestedPostApproval`, `updatePaidMessagesPrice` | Partial (no real payout ledger) | +| Statistics (channel / megagroup / message / story) | Full | `stats.getBroadcastStats`, `getMegagroupStats`, `getMessageStats`, `loadAsyncGraph` | Partial (graphs from local data) | +| Communities / peer links (layer 228) | Full | `communities.*` create / join / peer links / bans | Partial (new API) | + +## 6. Media and files + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Upload / download, big-file parts, file hashes | Full | `upload.saveFilePart` / `saveBigFilePart` / `getFile` / `getFileHashes` | Full | +| CDN-backed downloads, `upload.getCdnFile` | Full | None - always served from origin | None (by design) | +| Storage backends | Google infra | Local disk **or** S3/MinIO-compatible, switchable per deployment; low-space guard; stale-media cleanup | Full (self-host) | +| Web files / proxied external media (`upload.getWebFile`) | Full | Registered, external media fetch | Partial | +| Photos, documents, thumbnails, GIFv conversion, video | Full | Implemented incl. canonical GIFv conversion | Full | +| Web page previews, instant view | Full | Previews yes; **Instant View pages** no | Partial | +| Map/venue tile cache | Full | Cache hooks only | Partial | + +## 7. Stickers, emoji, GIFs + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Sticker sets install/archive/reorder, custom emoji, masks | Full | `messages.*StickerSet*`, `getCustomEmojiDocuments`, mask stickers | Full | +| Create / edit own sticker set (`stickers.*`) | Full | `createStickerSet`, `addStickerToSet`, `renameStickerSet`, suggest short name | Full | +| Featured / trending / recent / faved stickers | Full | `getFeaturedStickers`, `getRecentStickers`, `getFavedStickers` | Full | +| Emoji keywords / groups / status | Full | `getEmojiKeywords*`, `getEmojiGroups`, emoji status incl. collectible | Full | +| Saved GIFs + inline `@gif` catalog | Full | Admin-curated categorized `@gif` catalog, auto-save on send | Full (plus extras) | +| Premium animated emoji / effects | Full | `messages.getAvailableEffects` | Partial | + +## 8. Bots and mini apps + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Bot messaging, callbacks, inline mode | Full | `getInlineBotResults`, `sendInlineBotResult`, `getBotCallbackAnswer`, `setBotCallbackAnswer` | Full | +| BotFather-style bot creation & config | Full | `bots.createBot`, `setBotInfo`, `setBotCommands`, menu button, usernames | Full | +| Web apps / mini apps (`messages.requestWebView`, main/app/simple) | Full | `webViewRequest`, `mainWebViewRequest`, `appWebViewRequest`, `prolongWebView`, `sendWebViewData` | Full | +| Attachment-menu bots | Full | `getAttachMenuBots`, `toggleBotInAttachMenu` | Full | +| Bot API HTTP gateway (getUpdates / webhooks) | Full (`api.telegram.org`) | Minimal Bot API gateway in `internal/botapi`; persistent `getUpdates`, webhook delivery | Partial (subset of methods) | +| Business connections / Business AI replies | Full | `account.getBotBusinessConnection`, connected bots, business automation, AI echo | Partial | +| Bot payments (`payments.sendPaymentForm`, invoices) | Full | **Not registered** | None | +| Star-ref / affiliate programs | Full | `bots.updateStarRefProgram` shell | Stub | +| Games (`messages.setGameScore`, high scores) | Full | Registered incl. inline high scores | Full | + +## 9. Calls and live streams + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| 1:1 call signaling (DH, `phone.requestCall` ... `discardCall`) | Full | Full state machine with g_a hash commit, ring timeout, tombstones (`internal/app/phone`) | Full (signaling) | +| Group calls / voice chats | Full | `phone.joinGroupCall`, participants, `editGroupCallParticipant`, titles, scheduled starts | Full (signaling/state) | +| Conference calls (layer 228 chain blocks) | Full | `createConferenceCall`, `getGroupCallChainBlocks`, invite/decline | Partial | +| Media relay | Global TURN + SFU fleet | SFU/TURN **building blocks** (`internal/sfu`, `internal/turnsrv`); operator must run relays | Partial | +| RTMP live streaming into channels | Full | `phone.getGroupCallStreamRtmpUrl`, RTMP ingest, segmenter (`internal/app/livestream`) | Full | +| Screen sharing / presentation | Full | `joinGroupCallPresentation` / `leaveGroupCallPresentation` | Full (signaling) | +| Call debug / rating | Full | `saveCallDebug`, `setCallRating` | Full | + +## 10. Stories + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Post / edit / delete stories, media | Full | `stories.sendStory`, `editStory`, `deleteStories`, `canSendStory` | Full | +| Read state, views, viewers list, reactions | Full | `readStories`, `incrementStoryViews`, `getStoryViewsList`, `sendReaction` | Full | +| Pinned stories / profile grid, archive | Full | `togglePinned`, `getPinnedStories`, `getStoriesArchive` | Full | +| Story albums (layer 228) | Full | `createAlbum`, `getAlbumStories`, `reorderAlbums` | Full | +| Stealth mode, hidden peers | Full | `activateStealthMode`, `toggleAllStoriesHidden`, `togglePeerStoriesHidden` | Full | +| Channel stories + live stories | Full | Channel posting via access checks; `stories.startLive` | Partial | +| Story boosts / repost / public forwards | Full | `stats.getStoryPublicForwards`, search posts | Partial | + +## 11. Dialogs, sync, folders + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Dialog list, pinned, manual unread, archive folder | Full | `getDialogs`, `getPinnedDialogs`, `markDialogUnread`, `folders.editPeerFolders` | Full | +| Chat folders / dialog filters, suggested filters | Full | `getDialogFilters`, `updateDialogFilter`, `getSuggestedDialogFilters`, filter tags | Full | +| Shareable folders / chatlist invites | Full | `chatlists.exportChatlistInvite`, join/import, updates, revoked handling | Full | +| Update sequencing: `updates.getState` / `getDifference` / `getChannelDifference` | Full | Implemented incl. durable updates, `pts`/`qts`/`seq`, offline difference recovery | Full | +| Real-time push over the MTProto connection | Full | Online fan-out, reliable dispatch | Full | +| **External push (APNs / FCM / GCM)** | Full | **None** - `account.registerDevice` only records the in-connection MTProto push session; other token types are ignored | None (by design) | +| Notification settings / exceptions / reactions-notify | Full | `getNotifySettings`, `getNotifyExceptions`, `getReactionsNotifySettings` | Full | + +## 12. Secret chats (E2E) + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| DH handshake, `messages.requestEncryption` -> accept/discard | Full | State machine + id/access_hash allocation, blind g_a storage (`internal/app/secretchat`) | Full | +| Encrypted message / file / service delivery, `qts` queue | Full | `sendEncrypted`, `sendEncryptedFile`, `sendEncryptedService`, `uploadEncryptedFile` | Full | +| Encrypted typing, read history, spam report | Full | `setEncryptedTyping`, `readEncryptedHistory`, `reportEncryptedSpam` | Full | +| Rekeying / perfect forward secrecy for long chats | Full | Handled at handshake layer; server is a blind relay | Partial | + +## 13. Themes, wallpapers, appearance + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Wallpapers install/upload/reset, multi-wallpaper | Full | `account.getWallPapers`, `installWallPaper`, `getMultiWallPapers` | Full | +| Custom themes create/install/update, chat themes | Full | `account.createTheme`, `installTheme`, `getChatThemes`, `messages.setChatTheme` | Full | +| Peer colors, profile colors, name colors | Full | `help.getPeerColors`, `account.updateColor`, `channels.updateColor` | Full | +| Unique-gift chat themes | Full | `account.getUniqueGiftChatThemes` (empty - no gifts) | Stub | +| Ringtones | Full | `account.getSavedRingtones` | Full | + +## 14. Payments, Stars, gifts, Premium - deliberately omitted + +Telegram's economy layer is intentionally **not implemented**. OwpenGram keeps +a handful of read-only RPCs answered with valid empty/zero responses purely so +the official clients render "no Stars / no gifts" instead of hanging or +retrying in a storm. + +| Area | Telegram | OwpenGram | +|---|---|---| +| Invoice / payment form / shipping / checkout | Full (`payments.sendPaymentForm`, ...) | Not registered | +| Telegram Stars balance, purchase, transactions | Full | `getStarsStatus` / `Subscriptions` / `Transactions` return zero balance, empty ledger | +| Star gifts / collectible gifts / gift profiles | Full | `getStarGifts` / `getSavedStarGifts` return empty lists | +| Google Play / App Store receipt verification | Full | `canPurchaseStore` returns `false`; `assignPlayMarketTransaction` -> `STORE_PAYMENT_UNAVAILABLE` | +| Premium subscription, gift codes, giveaways | Full | `payments.getPremiumGiftCodeOptions` empty; Premium status can be granted server-side by the operator, not purchased | +| TON / channel revenue withdrawal | Full | `getStarsRevenueStats` / `RevenueAdsAccountUrl` return fixed zero/compat values | +| Business Stars / paid messages payout | Full | Price fields tracked; no real ledger or payout | + +## 15. Moderation, admin, operations + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Report spam / peer / message / reaction / profile photo | Full | `messages.report`, `account.reportPeer`, `reportProfilePhoto`, `channels.reportSpam` | Full | +| Global ban / spam-bot / account restrictions | Full (internal) | Per-account freeze (admin read-only restriction advertised via appConfig), moderation cases, appeal links (`internal/app/moderation`) | Partial | +| Anti-spam service for groups | Full (`@GroupAnti-SpamBot`) | `channels.toggleAntiSpam`, `reportAntiSpamFalsePositive` | Partial | +| Admin API + web UI | Not public | RBAC-scoped admin API tokens, web UI (`internal/adminapi`, `internal/web`) | Extra | +| Broadcast / announcements to all users | Not public | Admin-panel broadcast from the official account to all or a picked list | Extra | +| Shared-device detection across accounts | Internal | Implemented | Extra | +| TUI server panel (setup wizard, start/stop, git-pull update, log tail, .env editor) | N/A | Bundled (`tui-panel`) | Extra | +| Metrics, pprof, DB tracing, load-test harness | Internal | `internal/observability`, `internal/loadtest`, `internal/loadharness` | Extra | + +## 16. Verification and badges + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| Official blue checkmark | Full (manual) | `@verifybot` flow, verification worker | Full (self-host policy) | +| Third-party bot verification mark (icon + label before a name) | Full (`bots.setCustomVerification`) | `@marksbot` mechanism - **experimental, hidden by default** | Partial | +| Scam / fake labels | Full | Flag paths present | Partial | + +## 17. Misc API surface + +| Area | Telegram | OwpenGram | Status | +|---|---|---|---| +| `help.getConfig` / `getAppConfig` / `getCountriesList` / timezones / promo | Full | Implemented | Full | +| Language packs (`langpack.*`) | Full | `getLangPack`, `getDifference`, `getStrings`, seeded packs | Full | +| App update check (`help.getAppUpdate`) | Full | Registered | Partial (operator-fed) | +| Deep-link info, t.me link resolution | Full | `help.getDeepLinkInfo`, public link landing pages | Full | +| `contacts.getLocated` (nearby people/chats) | Full | Not registered | None | +| Peer color / emoji-status catalogs | Full | Implemented | Full | +| `smsjobs.*` (Android SMS relay income) | Full | Not registered | None | + +--- + +## Summary + +**Strong, near-complete parity:** + +- MTProto edge, auth, sessions, PFS, 2FA/SRP +- Private chat messaging: send/edit/delete/forward/reply/reactions/scheduled/ + drafts/search/translation/albums/rich text +- Groups, supergroups, channels: admin model, invite links, forum topics, + discussion groups, slow mode, anti-spam toggles, public search +- Media pipeline with pluggable local-disk or S3 storage +- Stickers / custom emoji / GIFs (with an extended `@gif` catalog) +- Bots, inline mode, web apps / mini apps, games +- Stories (incl. layer 228 albums, stealth mode) +- Secret chats (E2E, server as blind relay) +- Dialogs/folders/chatlists and the full update-difference sync machinery +- Themes, wallpapers, peer colors, language packs + +**Partial / compatibility-first:** + +- Calls and live streams: signaling and state are complete; media relay needs + an operator-run SFU/TURN +- Boosts, channel/story statistics, communities, business features, conference + calls: core flows present, some perks and edges not gated +- Bot API HTTP gateway: a useful subset, not the full `api.telegram.org` +- Moderation: instance-level tooling exists, no global trust-and-safety network + +**Deliberately not implemented:** + +- The entire payments/economy layer: invoices, Telegram Stars, star gifts, + Premium purchase, TON/revenue withdrawal, Play/App Store billing. Read-only + RPCs return valid empty responses so clients don't break. +- External push notifications (APNs / FCM / GCM). Delivery is only over the + live MTProto connection; `registerDevice` records the in-connection push + session and ignores other token types. +- Multi-DC / CDN infrastructure: one logical DC, files always served from + origin, no DC-migration redirects. +- `contacts.getLocated` (nearby), takeout sessions, `smsjobs.*`, Instant View. + +**OwpenGram-only additions not in Telegram's server:** + +- WebAuthn / passkey sign-in +- Self-hosted "Login with Telegram" OpenID Connect provider +- Admin API + web UI, RBAC admin tokens, admin broadcast/announcements +- Shared-device detection +- Bundled TUI server-operations panel +- AI compose / rewrite with pluggable local or external providers +- Pluggable S3/MinIO media backend with live switch diff --git a/go.mod b/go.mod index ebfec292..f645086f 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/golang-migrate/migrate/v4 v4.19.1 github.com/gotd/ige v0.3.0 github.com/gotd/log/logzap v0.1.1 - github.com/iamxvbaba/td v1.2.1 + github.com/iamxvbaba/td v1.3.2 github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa github.com/jackc/pgx/v5 v5.9.2 github.com/lestrrat-go/jwx/v3 v3.1.1 diff --git a/go.sum b/go.sum index 54be9510..8f268240 100644 --- a/go.sum +++ b/go.sum @@ -83,8 +83,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/iamxvbaba/td v1.2.1 h1:5+Ji1F/tdrN8zUxeeEbTPHBQGSnTDE+UAH+8pQi7O1Y= -github.com/iamxvbaba/td v1.2.1/go.mod h1:INkZJi18XbXtVOrldDnPtmQCJvIXhgDZdbo9MV/NY7M= +github.com/iamxvbaba/td v1.3.2 h1:/EwvDU0oiArAof16WDDGfUXg/w02m2MqzDskCnXakZA= +github.com/iamxvbaba/td v1.3.2/go.mod h1:INkZJi18XbXtVOrldDnPtmQCJvIXhgDZdbo9MV/NY7M= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= diff --git a/internal/admin/service.go b/internal/admin/service.go index 69ece289..17ee6d22 100644 --- a/internal/admin/service.go +++ b/internal/admin/service.go @@ -66,6 +66,9 @@ const ( ActionTransferCollectibleUsername = "usernames.collectible.transfer" ActionRevokeCollectibleUsername = "usernames.collectible.revoke" ActionDeleteCollectibleUsername = "usernames.collectible.delete" + // Operator username blocklist. + ActionReserveUsername = "usernames.reserve" + ActionUnreserveUsername = "usernames.unreserve" // Official platform verification review. Claim/approve/reject act on one // application; revoke acts on a target, because clearing a badge is not a // decision on the application that granted it. @@ -363,6 +366,16 @@ type CollectibleUsernamesService interface { Transfers(ctx context.Context, collectibleID int64, limit int) ([]domain.CollectibleUsernameTransfer, error) } +// ReservedUsernamesService is the operator username blocklist: a plain list of +// names no peer may take. Separate from the collectible lifecycle - a reservation +// has no owner, no price and no Fragment badge. +type ReservedUsernamesService interface { + IsReserved(ctx context.Context, usernameLower string) (bool, error) + ReserveUsername(ctx context.Context, username, reason, actor string) (created bool, err error) + UnreserveUsername(ctx context.Context, username string) (removed bool, err error) + ReservedUsernames(ctx context.Context, filter domain.ReservedUsernameFilter) ([]domain.ReservedUsername, error) +} + // collectibleUsernameByIDLookup is the optional by-identity read. Stores that // expose it answer a detail request in one round trip; the keyset fallback in // CollectibleUsernameByID keeps a service without it correct. @@ -389,6 +402,7 @@ type Dependencies struct { Emoji EmojiService Moderation ModerationService Usernames CollectibleUsernamesService + ReservedUsernames ReservedUsernamesService Verification VerificationService // BotVerification is the third-party mechanism, wired separately from // Verification: the two never read each other's state. @@ -420,6 +434,7 @@ type Service struct { emoji EmojiService moderation ModerationService usernames CollectibleUsernamesService + reservedUsernames ReservedUsernamesService verification VerificationService botVerification BotVerificationService account AccountService @@ -487,6 +502,9 @@ func (s *Service) Configure(deps Dependencies) *Service { if deps.Usernames != nil { s.usernames = deps.Usernames } + if deps.ReservedUsernames != nil { + s.reservedUsernames = deps.ReservedUsernames + } if deps.Verification != nil { s.verification = deps.Verification } @@ -2059,6 +2077,91 @@ func (s *Service) DeleteCollectibleUsername(ctx context.Context, req DeleteColle }) } +// ReserveUsernameRequest / UnreserveUsernameRequest add or remove a blocklist +// entry. reservedUsernameFromRequest normalises the name; the reason is a free +// operator note. +type ReserveUsernameRequest struct { + CommandMeta + Username string +} + +type UnreserveUsernameRequest struct { + CommandMeta + Username string +} + +// ReserveUsername adds a name to the operator blocklist. Journalled and +// replay-safe like every other command. +func (s *Service) ReserveUsername(ctx context.Context, req ReserveUsernameRequest) (CommandResult, error) { + if s == nil || s.reservedUsernames == nil { + return CommandResult{}, fmt.Errorf("admin reserved username dependency is not configured") + } + req.Username = domain.NormalizeUsername(req.Username) + if !domain.ValidCollectibleUsername(req.Username) { + return CommandResult{}, codedError(CodeUsernameInvalid, domain.ErrUsernameInvalid) + } + if len(req.Reason) > domain.MaxReservedUsernameReasonLength { + return CommandResult{}, fmt.Errorf("reason must be <= %d bytes", domain.MaxReservedUsernameReasonLength) + } + return s.runCommand(ctx, req.CommandMeta, ActionReserveUsername, 0, domain.Peer{}, req, func() (CommandResult, error) { + details := map[string]any{"username": req.Username} + if s.usernames != nil { + if asset, err := s.usernames.Collectible(ctx, req.Username); err == nil { + details["existing_collectible_id"] = strconv.FormatInt(asset.ID, 10) + return CommandResult{Details: details}, codedError(CodeUsernameOccupied, domain.ErrUsernameOccupied) + } + } + if req.DryRun { + return CommandResult{Message: "username reservation validated", Details: details}, nil + } + created, err := s.reservedUsernames.ReserveUsername(ctx, req.Username, req.Reason, req.Actor) + if err != nil { + return CommandResult{Details: details}, err + } + details["created"] = created + message := "username reserved" + if !created { + message = "username was already reserved" + } + return CommandResult{Message: message, Details: details}, nil + }) +} + +// UnreserveUsername removes a name from the operator blocklist. +func (s *Service) UnreserveUsername(ctx context.Context, req UnreserveUsernameRequest) (CommandResult, error) { + if s == nil || s.reservedUsernames == nil { + return CommandResult{}, fmt.Errorf("admin reserved username dependency is not configured") + } + req.Username = domain.NormalizeUsername(req.Username) + if strings.TrimSpace(req.Username) == "" { + return CommandResult{}, codedError(CodeUsernameInvalid, domain.ErrUsernameInvalid) + } + return s.runCommand(ctx, req.CommandMeta, ActionUnreserveUsername, 0, domain.Peer{}, req, func() (CommandResult, error) { + details := map[string]any{"username": req.Username} + if req.DryRun { + return CommandResult{Message: "username unreservation validated", Details: details}, nil + } + removed, err := s.reservedUsernames.UnreserveUsername(ctx, req.Username) + if err != nil { + return CommandResult{Details: details}, err + } + details["removed"] = removed + message := "username unreserved" + if !removed { + message = "username was not reserved" + } + return CommandResult{Message: message, Details: details}, nil + }) +} + +// ReservedUsernames is the admin listing read for the blocklist. +func (s *Service) ReservedUsernames(ctx context.Context, filter domain.ReservedUsernameFilter) ([]domain.ReservedUsername, error) { + if s == nil || s.reservedUsernames == nil { + return nil, fmt.Errorf("reserved username dependency is not configured") + } + return s.reservedUsernames.ReservedUsernames(ctx, filter) +} + func collectibleOwnerPeer(userID, channelID int64) (domain.Peer, error) { if userID < 0 || channelID < 0 { return domain.Peer{}, fmt.Errorf("owner id must be positive") diff --git a/internal/admin/service_test.go b/internal/admin/service_test.go index 06699284..c9e564f7 100644 --- a/internal/admin/service_test.go +++ b/internal/admin/service_test.go @@ -12,6 +12,7 @@ import ( usernamesapp "telesrv/internal/app/usernames" "telesrv/internal/domain" + "telesrv/internal/store/memory" ) // Compile-time proof that the shipped use-case services satisfy the admin ports. @@ -1427,3 +1428,76 @@ func TestDeleteCollectibleUsernameCommand(t *testing.T) { t.Fatalf("delete of invalid name = nil error, want rejection") } } + +func TestReserveAndUnreserveUsername(t *testing.T) { + ctx := context.Background() + reserved := memory.NewReservedUsernameStore() + svc := NewService(Dependencies{ + Commands: newMemoryCommandRepo(), + ReservedUsernames: reserved, + Now: fixedNow, + }) + + dry, err := svc.ReserveUsername(ctx, ReserveUsernameRequest{ + CommandMeta: CommandMeta{CommandID: "rv-dry", Actor: "ops", Reason: "official handle", DryRun: true}, + Username: "@Support", + }) + if err != nil { + t.Fatalf("dry-run reserve: %v", err) + } + if got, _ := reserved.IsReserved(ctx, "support"); got { + t.Fatal("dry-run reserved the name") + } + if dry.Details["username"] != "Support" { + t.Fatalf("dry-run details = %+v", dry.Details) + } + + if _, err := svc.ReserveUsername(ctx, ReserveUsernameRequest{ + CommandMeta: CommandMeta{CommandID: "rv-exec", Actor: "ops", Reason: "official handle"}, + Username: "support", + }); err != nil { + t.Fatalf("reserve: %v", err) + } + if got, _ := reserved.IsReserved(ctx, "support"); !got { + t.Fatal("name not reserved after exec") + } + + if _, err := svc.UnreserveUsername(ctx, UnreserveUsernameRequest{ + CommandMeta: CommandMeta{CommandID: "urv-exec", Actor: "ops", Reason: "no longer needed"}, + Username: "SUPPORT", + }); err != nil { + t.Fatalf("unreserve: %v", err) + } + if got, _ := reserved.IsReserved(ctx, "support"); got { + t.Fatal("name still reserved after unreserve") + } + + if _, err := svc.ReserveUsername(ctx, ReserveUsernameRequest{ + CommandMeta: CommandMeta{CommandID: "bad", Actor: "ops", Reason: "x"}, + Username: "ab", + }); err == nil { + t.Fatal("reserve of a too-short name = nil error, want rejection") + } +} + +func TestMemoryRegistryRefusesReservedName(t *testing.T) { + ctx := context.Background() + reserved := memory.NewReservedUsernameStore() + if _, err := reserved.ReserveUsername(ctx, "support", "", "ops"); err != nil { + t.Fatalf("seed reserve: %v", err) + } + registry := memory.NewCollectibleUsernameStore().WithReservedUsernames(reserved) + + if _, err := registry.SetEditableUsername(ctx, domain.Peer{Type: domain.PeerTypeUser, ID: 1}, "support"); !errors.Is(err, domain.ErrUsernameOccupied) { + t.Fatalf("SetEditableUsername(reserved) err = %v, want ErrUsernameOccupied", err) + } + if _, _, err := registry.MintCollectibleUsername(ctx, domain.MintCollectibleUsernameRequest{ + Username: "support", Currency: domain.CollectibleCurrencyUSD, Amount: 0, CommandKey: "k1", + }); !errors.Is(err, domain.ErrUsernameOccupied) { + t.Fatalf("Mint(reserved) err = %v, want ErrUsernameOccupied", err) + } + // A different name is unaffected. + if _, err := registry.SetEditableUsername(ctx, domain.Peer{Type: domain.PeerTypeUser, ID: 1}, "freename"); err != nil { + t.Fatalf("SetEditableUsername(free) err = %v", err) + } +} diff --git a/internal/adminapi/server.go b/internal/adminapi/server.go index 678535bb..0a3d968f 100644 --- a/internal/adminapi/server.go +++ b/internal/adminapi/server.go @@ -98,6 +98,9 @@ type Service interface { CollectibleUsernames(ctx context.Context, filter domain.CollectibleUsernameFilter) ([]domain.CollectibleUsername, error) CollectibleUsernameByID(ctx context.Context, id int64) (domain.CollectibleUsername, error) CollectibleUsernameTransfers(ctx context.Context, collectibleID int64, limit int) ([]domain.CollectibleUsernameTransfer, error) + ReserveUsername(ctx context.Context, req admin.ReserveUsernameRequest) (admin.CommandResult, error) + UnreserveUsername(ctx context.Context, req admin.UnreserveUsernameRequest) (admin.CommandResult, error) + ReservedUsernames(ctx context.Context, filter domain.ReservedUsernameFilter) ([]domain.ReservedUsername, error) ClaimVerification(ctx context.Context, req admin.ClaimVerificationRequest) (admin.CommandResult, error) ApproveVerification(ctx context.Context, req admin.ApproveVerificationRequest) (admin.CommandResult, error) RejectVerification(ctx context.Context, req admin.RejectVerificationRequest) (admin.CommandResult, error) @@ -234,6 +237,9 @@ func (s *Server) routes() http.Handler { mux.HandleFunc("POST /v1/collectible-usernames/delete", s.authenticated(s.handleDeleteCollectibleUsername)) mux.HandleFunc("GET /v1/collectible-usernames", s.authenticated(s.handleCollectibleUsernames)) mux.HandleFunc("GET /v1/collectible-usernames/{id}", s.authenticated(s.handleCollectibleUsername)) + mux.HandleFunc("POST /v1/reserved-usernames/reserve", s.authenticated(s.handleReserveUsername)) + mux.HandleFunc("POST /v1/reserved-usernames/unreserve", s.authenticated(s.handleUnreserveUsername)) + mux.HandleFunc("GET /v1/reserved-usernames", s.authenticated(s.handleReservedUsernames)) // Official platform verification. Unlike every route above, these carry a // named permission, so a scoped token can be given the review surface and // nothing else. Revocation additionally requires verification.revoke. @@ -1186,6 +1192,54 @@ func (s *Server) handleDeleteCollectibleUsername(w http.ResponseWriter, r *http. writeCommandResult(w, result, err) } +func (s *Server) handleReserveUsername(w http.ResponseWriter, r *http.Request) { + var req admin.ReserveUsernameRequest + if !decodeJSON(w, r, &req) { + return + } + result, err := s.svc.ReserveUsername(r.Context(), req) + writeCommandResult(w, result, err) +} + +func (s *Server) handleUnreserveUsername(w http.ResponseWriter, r *http.Request) { + var req admin.UnreserveUsernameRequest + if !decodeJSON(w, r, &req) { + return + } + result, err := s.svc.UnreserveUsername(r.Context(), req) + writeCommandResult(w, result, err) +} + +func (s *Server) handleReservedUsernames(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + filter := domain.ReservedUsernameFilter{Query: query.Get("q")} + limit, ok := optionalQueryInt(w, query, "limit") + if !ok { + return + } + filter.Limit = limit + offset, ok := optionalQueryInt(w, query, "offset") + if !ok { + return + } + filter.Offset = offset + items, err := s.svc.ReservedUsernames(r.Context(), filter) + if err != nil { + writeError(w, http.StatusInternalServerError, "list failed") + return + } + out := make([]map[string]any, 0, len(items)) + for _, item := range items { + out = append(out, map[string]any{ + "username": item.Username, + "reason": item.Reason, + "actor": item.Actor, + "created_at": item.CreatedAt.Unix(), + }) + } + writeJSON(w, http.StatusOK, map[string]any{"reserved": out}) +} + func (s *Server) handleCollectibleUsernames(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() filter := domain.CollectibleUsernameFilter{ diff --git a/internal/adminapi/server_test.go b/internal/adminapi/server_test.go index 61029dd7..dffebdb1 100644 --- a/internal/adminapi/server_test.go +++ b/internal/adminapi/server_test.go @@ -510,12 +510,30 @@ func (fakeService) ReviewModerationAppeal(context.Context, domain.ModerationDeci type captureCollectibleUsernameService struct { fakeService - mint admin.MintCollectibleUsernameRequest - transfer admin.TransferCollectibleUsernameRequest - revoke admin.RevokeCollectibleUsernameRequest - del admin.DeleteCollectibleUsernameRequest - filter domain.CollectibleUsernameFilter - assetID int64 + mint admin.MintCollectibleUsernameRequest + transfer admin.TransferCollectibleUsernameRequest + revoke admin.RevokeCollectibleUsernameRequest + del admin.DeleteCollectibleUsernameRequest + reserve admin.ReserveUsernameRequest + unreserve admin.UnreserveUsernameRequest + resFilter domain.ReservedUsernameFilter + filter domain.CollectibleUsernameFilter + assetID int64 +} + +func (s *captureCollectibleUsernameService) ReserveUsername(_ context.Context, req admin.ReserveUsernameRequest) (admin.CommandResult, error) { + s.reserve = req + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + +func (s *captureCollectibleUsernameService) UnreserveUsername(_ context.Context, req admin.UnreserveUsernameRequest) (admin.CommandResult, error) { + s.unreserve = req + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + +func (s *captureCollectibleUsernameService) ReservedUsernames(_ context.Context, filter domain.ReservedUsernameFilter) ([]domain.ReservedUsername, error) { + s.resFilter = filter + return []domain.ReservedUsername{{Username: "support", Reason: "official", Actor: "ops"}}, nil } func (s *captureCollectibleUsernameService) MintCollectibleUsername(_ context.Context, req admin.MintCollectibleUsernameRequest) (admin.CommandResult, error) { @@ -731,3 +749,49 @@ func (fakeService) CollectibleUsernameByID(context.Context, int64) (domain.Colle func (fakeService) CollectibleUsernameTransfers(context.Context, int64, int) ([]domain.CollectibleUsernameTransfer, error) { return nil, nil } + +func (fakeService) ReserveUsername(_ context.Context, req admin.ReserveUsernameRequest) (admin.CommandResult, error) { + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + +func (fakeService) UnreserveUsername(_ context.Context, req admin.UnreserveUsernameRequest) (admin.CommandResult, error) { + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + +func (fakeService) ReservedUsernames(context.Context, domain.ReservedUsernameFilter) ([]domain.ReservedUsername, error) { + return nil, nil +} + +func TestAdminAPIReservedUsernames(t *testing.T) { + svc := &captureCollectibleUsernameService{} + srv := &Server{token: "secret", svc: svc} + + reserve := httptest.NewRequest(http.MethodPost, "/v1/reserved-usernames/reserve", strings.NewReader( + `{"command_id":"r-1","actor":"ops","reason":"official","username":"support"}`)) + reserve.Header.Set("Authorization", "Bearer secret") + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, reserve) + if rec.Code != http.StatusOK || svc.reserve.Username != "support" || svc.reserve.CommandID != "r-1" { + t.Fatalf("reserve status=%d req=%+v", rec.Code, svc.reserve) + } + + unreserve := httptest.NewRequest(http.MethodPost, "/v1/reserved-usernames/unreserve", strings.NewReader( + `{"command_id":"u-1","actor":"ops","reason":"done","username":"support"}`)) + unreserve.Header.Set("Authorization", "Bearer secret") + rec = httptest.NewRecorder() + srv.routes().ServeHTTP(rec, unreserve) + if rec.Code != http.StatusOK || svc.unreserve.Username != "support" { + t.Fatalf("unreserve status=%d req=%+v", rec.Code, svc.unreserve) + } + + list := httptest.NewRequest(http.MethodGet, "/v1/reserved-usernames?q=sup&limit=10", nil) + list.Header.Set("Authorization", "Bearer secret") + rec = httptest.NewRecorder() + srv.routes().ServeHTTP(rec, list) + if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"username":"support"`) { + t.Fatalf("list status=%d body=%s", rec.Code, rec.Body.String()) + } + if svc.resFilter.Query != "sup" || svc.resFilter.Limit != 10 { + t.Fatalf("list filter = %+v", svc.resFilter) + } +} diff --git a/internal/app/account/lifecycle.go b/internal/app/account/lifecycle.go index e59954f0..60168bb2 100644 --- a/internal/app/account/lifecycle.go +++ b/internal/app/account/lifecycle.go @@ -80,7 +80,7 @@ func (s *Service) DeleteAccount(ctx context.Context, userID int64, authKeyID [8] } executeAt := now.Add(accountDeletionDelay) message := fmt.Sprintf( - "A request was made to delete your "+branding.ProductName+" account. If this wasn't you, cancel the request: tg://confirmphone?phone=%s&hash=%s", + "A request was made to delete your "+branding.ProductName()+" account. If this wasn't you, cancel the request: tg://confirmphone?phone=%s&hash=%s", url.QueryEscape(snapshot.User.Phone), url.QueryEscape(rawToken), ) pending, _, err := s.lifecycle.ScheduleAccountDeletion(ctx, domain.ScheduleAccountDeletion{ diff --git a/internal/app/account/service.go b/internal/app/account/service.go index 03ab3f4d..b2165181 100644 --- a/internal/app/account/service.go +++ b/internal/app/account/service.go @@ -271,18 +271,26 @@ func (s *Service) GetPassword(ctx context.Context, userID int64) (domain.Passwor return defaultPasswordSettings(), nil } settings = normalizePasswordSettings(settings) - if settings.HasPassword { + // Only mint a fresh SRP challenge (secret/B/SRPID together) when none is + // outstanding yet. Regenerating B on every read while leaving SRPID + // untouched let any two account.getPassword calls silently invalidate each + // other's B without a signal the client could detect (SRPID unchanged) -- + // a client that fetched the password state, had a second screen/dialog + // refresh it again, then submitted against the first B it saw got a false + // PASSWORD_HASH_INVALID even with the correct password. The challenge must + // stay stable across reads and only rotate when it's actually consumed by + // a password change (UpdatePasswordSettings/RecoverPassword already mint + // their own fresh challenge there). + if settings.HasPassword && (len(settings.SRPBSecret) == 0 || len(settings.SRPB) == 0 || settings.SRPID == 0) { secret, b, err := makeSRPChallenge(settings.SRPVerifier) if err != nil { return domain.PasswordSettings{}, err } settings.SRPBSecret = secret settings.SRPB = b - if settings.SRPID == 0 { - settings.SRPID, err = randomInt64() - if err != nil { - return domain.PasswordSettings{}, err - } + settings.SRPID, err = randomInt64() + if err != nil { + return domain.PasswordSettings{}, err } if err := s.passwords.Save(ctx, userID, settings); err != nil { return domain.PasswordSettings{}, err diff --git a/internal/app/account/service_test.go b/internal/app/account/service_test.go index 1689cf9e..f1eb7ef5 100644 --- a/internal/app/account/service_test.go +++ b/internal/app/account/service_test.go @@ -68,6 +68,54 @@ func TestPasswordSRPRoundTrip(t *testing.T) { } } +// TestGetPasswordChallengeStableAcrossReads guards against a real production +// bug: GetPassword used to mint a brand-new random SRP server secret/B on +// every single call while leaving SRPID untouched. Two account.getPassword +// calls in a row (e.g. a settings screen and, moments later, the transfer- +// ownership dialog's own cloudPassword().reload()) would silently invalidate +// each other's B with no signal the client could detect (SRPID unchanged), +// so a password check built from the first response's B failed with +// PASSWORD_HASH_INVALID even though the typed password was correct. The +// challenge must stay identical across reads until a real password change +// consumes it. +func TestGetPasswordChallengeStableAcrossReads(t *testing.T) { + ctx := context.Background() + const userID int64 = 1003 + svc := NewService(memory.NewPasswordStore()) + + initial, err := svc.GetPassword(ctx, userID) + if err != nil { + t.Fatalf("GetPassword initial: %v", err) + } + algo := initial.NewAlgo + algo.Salt1 = append(append([]byte(nil), algo.Salt1...), bytes.Repeat([]byte{0x11}, 32)...) + if err := svc.UpdatePasswordSettings(ctx, userID, domain.PasswordCheck{Empty: true}, domain.PasswordInputSettings{ + NewAlgo: &algo, + NewPasswordHash: verifierForPassword(algo, []byte("correct horse")), + }); err != nil { + t.Fatalf("UpdatePasswordSettings set password: %v", err) + } + + first, err := svc.GetPassword(ctx, userID) + if err != nil { + t.Fatalf("GetPassword first: %v", err) + } + // Simulate a second, unrelated screen/dialog refreshing the same cloud + // password state before the user submits their check. + second, err := svc.GetPassword(ctx, userID) + if err != nil { + t.Fatalf("GetPassword second: %v", err) + } + if first.SRPID != second.SRPID || !bytes.Equal(first.SRPB, second.SRPB) { + t.Fatalf("challenge changed across reads: first=%+v second=%+v, want identical SRPID/SRPB", first, second) + } + + check := clientPasswordCheck(t, first, []byte("correct horse")) + if err := svc.CheckPassword(ctx, userID, check); err != nil { + t.Fatalf("CheckPassword against first-read challenge after an intervening read: %v", err) + } +} + func TestRecoverPasswordClearsTwoFactorPassword(t *testing.T) { ctx := context.Background() const userID int64 = 1002 diff --git a/internal/app/auth/service.go b/internal/app/auth/service.go index 8330f31e..dc5d9d8a 100644 --- a/internal/app/auth/service.go +++ b/internal/app/auth/service.go @@ -1535,9 +1535,9 @@ func (s *Service) passwordNeeded(ctx context.Context, userID int64) (bool, error return found && settings.HasPassword, nil } -const loginMessageTpl = `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName + `! +var loginMessageTpl = `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName() + `! -This code can be used to log in to your ` + branding.ProductName + ` account. We never ask it for anything else. +This code can be used to log in to your ` + branding.ProductName() + ` account. We never ask it for anything else. If you didn't request this code by trying to log in on another device, simply ignore this message.` diff --git a/internal/app/bots/botfather.go b/internal/app/bots/botfather.go index 34657d55..4036c9a2 100644 --- a/internal/app/bots/botfather.go +++ b/internal/app/bots/botfather.go @@ -26,6 +26,7 @@ const ( botFatherCmdToken = "token" botFatherCmdRevoke = "revoke" botFatherCmdSetName = "setname" + botFatherCmdSetBotpic = "setbotpic" botFatherCmdSetDescription = "setdescription" botFatherCmdSetAbout = "setabouttext" botFatherCmdSetCommands = "setcommands" @@ -50,7 +51,7 @@ const ( maxTelegramLoginCommandsPerMessage = 32 ) -const botFatherHelpText = `I can help you create and manage ` + branding.ProductName + ` bots. +var botFatherHelpText = `I can help you create and manage ` + branding.ProductName() + ` bots. You can control me by sending these commands: @@ -115,7 +116,7 @@ func (s *Service) OnPrivateMessage(ctx context.Context, botUserID int64, msg dom } switch botUserID { case domain.BotFatherUserID: - go s.respondAsBotFather(userID, msg.Body) + go s.respondAsBotFather(userID, msg) case domain.StickersBotUserID: go s.respondAsStickers(userID, msg) case domain.ChatBotUserID: @@ -130,14 +131,14 @@ func (s *Service) OnPrivateMessage(ctx context.Context, botUserID int64, msg dom // respondAsBotFather 生成并写入 BotFather 回复(OnPrivateMessage 在 goroutine 内调用)。 // 按用户取条带锁串行:状态机 Get→modify→Upsert/Delete 的 RMW 因此原子、回复保序, // 不同用户并发不受影响。ctx 用 Background(脱离已返回的用户 RPC),限较长超时。 -func (s *Service) respondAsBotFather(userID int64, body string) { +func (s *Service) respondAsBotFather(userID int64, msg domain.Message) { mu := s.serviceBotReplyLock(domain.BotFatherUserID, userID) mu.Lock() defer mu.Unlock() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - reply := s.handleBotFather(ctx, userID, body) + reply := s.handleBotFather(ctx, userID, msg) s.sendServiceBotReply(ctx, domain.BotFatherUserID, userID, reply) } @@ -194,6 +195,45 @@ func (s *Service) sendServiceBotReplyResult(ctx context.Context, botUserID, user return res, true } +// editServiceBotMessage 就地改写一条内置 bot 自己发出的消息(正文 + inline keyboard)。 +// 用于按钮式菜单(@BotFather /mybots)在点击后原地翻页/下钻,而不是刷屏新消息。 +// OwnerUserID 取 bot 自身:store 的 authorEdit 判定要求 sender==from==owner,bot 发出的 +// 那一份 outbox 拷贝正好满足。ErrMessageNotModified 视为成功(点了同一个按钮两次)。 +func (s *Service) editServiceBotMessage(ctx context.Context, botUserID, userID int64, messageID int, reply botReply) bool { + if s == nil || s.messages == nil || messageID <= 0 || reply.Text == "" { + return false + } + markup := reply.ReplyMarkup + if err := domain.ValidateReplyMarkup(markup); err != nil { + s.log.Error("service bot: invalid reply markup for edit", + zap.Int64("bot_user_id", botUserID), zap.Int64("user_id", userID), zap.Error(err)) + markup = nil + } + if markup.IsZero() { + markup = nil + } + _, err := s.messages.EditMessage(ctx, domain.EditMessageRequest{ + OwnerUserID: botUserID, + Peer: domain.Peer{Type: domain.PeerTypeUser, ID: userID}, + ID: messageID, + Message: reply.Text, + Entities: serviceBotReplyEntities(reply.Text, reply.Entities), + EditDate: int(s.now().Unix()), + HideEdited: true, + SetReplyMarkup: true, + ReplyMarkup: markup, + }) + if errors.Is(err, domain.ErrMessageNotModified) { + return true + } + if err != nil { + s.log.Error("service bot: edit reply", + zap.Int64("bot_user_id", botUserID), zap.Int64("user_id", userID), zap.Error(err)) + return false + } + return true +} + // botReplyRandomID 为服务端回复构造非零幂等键((sender, random_id) 唯一索引)。 // 所有服务 bot 回复按各自 sender 命名空间唯一——用 // crypto/rand 取 64 位随机数(碰撞概率可忽略),熵源失败时退化为纳秒+单调序列。 @@ -222,8 +262,8 @@ var botFatherGlobalCommands = map[string]bool{ botFatherCmdSetLogin: true, botFatherCmdLoginInfo: true, botFatherCmdResetLogin: true, } -func (s *Service) handleBotFather(ctx context.Context, userID int64, text string) botReply { - text = strings.TrimSpace(text) +func (s *Service) handleBotFather(ctx context.Context, userID int64, msg domain.Message) botReply { + text := strings.TrimSpace(msg.Body) state, found, err := s.bots.GetBotChatState(ctx, domain.BotFatherUserID, userID) if err != nil { s.log.Error("botfather: get chat state", zap.Int64("user_id", userID), zap.Error(err)) @@ -234,12 +274,24 @@ func (s *Service) handleBotFather(ctx context.Context, userID int64, text string if cmd, ok := parseBotCommand(text); ok { inValueStep := found && state.Step == botFatherStepValue if !inValueStep || botFatherGlobalCommands[cmd] { + // "/start " (the "Manage Bot" deep link) jumps straight to that + // bot's menu, like /mybots then tapping the bot. + if cmd == "start" { + if arg := botCommandArg(text); arg != "" { + return s.handleBotFatherStart(ctx, userID, arg) + } + } return s.handleBotFatherCommand(ctx, userID, cmd) } } if text == "" { // 空白文本 / 贴纸 / 无 caption 媒体:有活动状态时回当前步骤提示, // 无状态保持沉默(避免对任意非文本消息刷屏)。 + if found && state.Step == botFatherStepValue && msg.Media != nil { + // A photo for /setbotpic arrives with no caption: hand the message to + // the value step instead of replaying the prompt. + return s.handleSetValue(ctx, state, msg) + } if !found { return botReply{} } @@ -256,7 +308,9 @@ func (s *Service) handleBotFather(ctx context.Context, userID int64, text string case state.Step == botFatherStepChoose: return s.handleChooseBot(ctx, state, text) case state.Step == botFatherStepValue: - return s.handleSetValue(ctx, state, text) + return s.handleSetValue(ctx, state, msg) + case state.Command == mybotsCommand: + return botReply{Text: "Please use the buttons in my message above, or send /mybots to open the list again."} default: // 不可达的脏状态:清掉重来,避免用户被卡死。 _ = s.bots.DeleteBotChatState(ctx, domain.BotFatherUserID, userID) @@ -314,11 +368,48 @@ func (s *Service) stepPrompt(state domain.BotChatState) botReply { return botReply{Text: "Please send the username of one of your bots, or /cancel."} case state.Step == botFatherStepValue: return botReply{Text: valuePrompt(state.Command, state.Draft[botFatherDraftBotUsername])} + case state.Command == mybotsCommand: + return botReply{Text: "Please use the buttons in my message above, or send /mybots to open the list again."} default: return botReply{Text: "Send /help for a list of commands."} } } +// handleBotFatherStart answers "/start ". When names one of the +// user's own bots (by username or numeric id) it opens that bot's menu - the +// same "What do you want to do?" screen as /mybots then tapping the bot, which +// is what the "Manage Bot" button on a bot's profile links to. An empty or +// unknown arg falls back to the plain greeting. +func (s *Service) handleBotFatherStart(ctx context.Context, userID int64, arg string) botReply { + _ = s.bots.DeleteBotChatState(ctx, domain.BotFatherUserID, userID) + want := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(arg), "@")) + if want == "" { + return botReply{Text: botFatherHelpText} + } + owned, err := s.ownedBots(ctx, userID) + if err != nil { + s.log.Error("botfather: list bots for start payload", zap.Int64("user_id", userID), zap.Error(err)) + return internalReply() + } + for _, b := range owned { + if strings.EqualFold(b.user.Username, want) || strconv.FormatInt(b.user.ID, 10) == want { + state := domain.BotChatState{ + BotUserID: domain.BotFatherUserID, + UserID: userID, + Command: mybotsCommand, + Step: mybotsStepMenu, + Draft: map[string]string{}, + } + reply := s.myBotsBotMenu(&state, b) + if !s.saveMyBotsState(ctx, state) { + return internalReply() + } + return reply + } + } + return botReply{Text: botFatherHelpText} +} + func (s *Service) handleBotFatherCommand(ctx context.Context, userID int64, cmd string) botReply { switch cmd { case "start", "help": @@ -363,15 +454,7 @@ func (s *Service) handleBotFatherCommand(ctx context.Context, userID int64, cmd } return botReply{Text: "Alright, a new bot. How are we going to call it? Please choose a name for your bot."} case "mybots": - usernames, err := s.ownedBotUsernames(ctx, userID) - if err != nil { - s.log.Error("botfather: list bots", zap.Int64("user_id", userID), zap.Error(err)) - return internalReply() - } - if len(usernames) == 0 { - return botReply{Text: "You don't have any bots yet. Use /newbot to create one."} - } - return botReply{Text: "Here are your bots:\n\n@" + strings.Join(usernames, "\n@")} + return s.startMyBots(ctx, userID) case botFatherCmdToken, botFatherCmdRevoke, botFatherCmdSetName, botFatherCmdSetDescription, botFatherCmdSetAbout, botFatherCmdSetCommands, botFatherCmdSetInline, botFatherCmdSetInlineGeo, @@ -391,6 +474,8 @@ func valuePrompt(cmd, username string) string { switch cmd { case botFatherCmdSetName: return fmt.Sprintf("OK. Send me the new name for @%s.", username) + case botFatherCmdSetBotpic: + return fmt.Sprintf("OK. Send me the new profile picture for @%s. Send it as a photo.", username) case botFatherCmdSetDescription: return fmt.Sprintf("OK. Send me the new description for @%s. People will see it on the bot's profile page, before they start a chat with it.", username) case botFatherCmdSetAbout: @@ -577,8 +662,10 @@ func (s *Service) handleChooseBot(ctx context.Context, state domain.BotChatState } } -// handleSetValue 处理选中 bot 后的收值步骤(/setname 等)。 -func (s *Service) handleSetValue(ctx context.Context, state domain.BotChatState, text string) botReply { +// handleSetValue 处理选中 bot 后的收值步骤(/setname 等)。除 /setbotpic 收一张 +// 照片外都是纯文本;照片经 msg.Media 传入。 +func (s *Service) handleSetValue(ctx context.Context, state domain.BotChatState, msg domain.Message) botReply { + text := strings.TrimSpace(msg.Body) botID, _ := strconv.ParseInt(state.Draft[botFatherDraftBotID], 10, 64) username := state.Draft[botFatherDraftBotUsername] if botID == 0 { @@ -618,6 +705,8 @@ func (s *Service) handleSetValue(ctx context.Context, state domain.BotChatState, reply, err = s.applyToggle(ctx, botID, text, true) case botFatherCmdSetPrivacy: reply, err = s.applyToggle(ctx, botID, text, false) + case botFatherCmdSetBotpic: + reply, err = s.applySetBotpic(ctx, botID, username, msg) case botFatherCmdSetLogin: return s.handleTelegramLoginConfigurationInput(ctx, state, botID, username, text) default: @@ -631,6 +720,11 @@ func (s *Service) handleSetValue(ctx context.Context, state domain.BotChatState, } return reply } + if state.Draft[mybotsDraftReturn] == "1" { + // Opened from the /mybots menu: land back on the Edit Bot menu (fresh + // message, working buttons) rather than ending the dialog. + return s.myBotsReturnToEditMenu(ctx, state.UserID, botID, state.Draft[mybotsDraftPage], reply.Text) + } s.clearState(ctx, state.UserID) return reply } @@ -956,6 +1050,25 @@ func (s *Service) applyTelegramLoginConfiguration(ctx context.Context, botID int return botReply{Text: telegramLoginConfigurationPrompt(username)}, domain.ErrTelegramLoginRequestInvalid } +// applySetBotpic 把用户发给 BotFather 的一张照片设为 bot 头像。只收 photo(不收 +// 文件/贴纸);文件层重渲染成头像尺寸集。 +func (s *Service) applySetBotpic(ctx context.Context, botID int64, username string, msg domain.Message) (botReply, error) { + if msg.Media == nil || msg.Media.Kind != domain.MessageMediaKindPhoto || msg.Media.Photo == nil || msg.Media.Photo.ID == 0 { + return botReply{Text: "Please send a photo (as a photo, not a file), or /cancel."}, domain.ErrPhotoInvalid + } + err := s.SetBotUserpic(ctx, botID, msg.Media.Photo.ID) + switch { + case errors.Is(err, ErrBotUserpicUnsupported): + return botReply{Text: "Setting a profile picture isn't available on this server."}, err + case errors.Is(err, domain.ErrPhotoInvalid): + return botReply{Text: "Sorry, I couldn't use that image. Send a JPEG or PNG photo, or /cancel."}, err + case err != nil: + s.log.Error("botfather: set botpic", zap.Int64("bot_user_id", botID), zap.Error(err)) + return botReply{}, err + } + return botReply{Text: fmt.Sprintf("Success! Profile picture updated for @%s.", username)}, nil +} + // applyToggle 解析 enable/disable 并设置 joingroups(join=true)或 privacy(join=false)。 func (s *Service) applyToggle(ctx context.Context, botID int64, text string, join bool) (botReply, error) { var on bool @@ -1086,3 +1199,16 @@ func parseBotCommand(text string) (string, bool) { } return strings.ToLower(cmd), true } + +// botCommandArg returns the trimmed argument after a leading "/cmd", e.g. +// "/start my_bot" -> "my_bot". Empty when there is no argument. +func botCommandArg(text string) string { + text = strings.TrimSpace(text) + if !strings.HasPrefix(text, "/") { + return "" + } + if i := strings.IndexAny(text, " \t\n"); i >= 0 { + return strings.TrimSpace(text[i+1:]) + } + return "" +} diff --git a/internal/app/bots/mybots.go b/internal/app/bots/mybots.go new file mode 100644 index 00000000..5b60d36c --- /dev/null +++ b/internal/app/bots/mybots.go @@ -0,0 +1,823 @@ +package bots + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "go.uber.org/zap" + + "telesrv/internal/domain" +) + +// @BotFather 的按钮式 /mybots:列出 owner 的 bot(每行一个 + 上一页/下一页),点进 +// 单个 bot 后是 API Token / Edit Bot / Bot Settings / Delete Bot 四个入口。除了 +// "Edit Bot" 下的文本字段(改名/简介/描述/命令列表,交回既有 handleSetValue 收值 +// 流程)之外,全部翻页与下钻都在同一条消息上就地 EditMessage,不刷屏。 +// +// 会话状态 domain.BotChatState(Command="mybots"),与其余 @BotFather 流程一样在 +// per-user serviceBotReplyLock 下 Get→改→Upsert,回调按用户串行、保序。 +// +// 安全:回调 data 只带一个随机 token,映射到 *该用户自己* BotChatState.Draft 里 +// 记下的 choice——data 里那个 bot id 是服务端渲染键盘时写进去的,不是客户端传的。 +// 即便如此,每个动作执行前仍重新按 owner 解析一次(myBotForUser 只返回 owner 名下 +// 的 bot),伪造/重放的 token 顶多命中一个不在自己表里的 key,被直接拒绝。 + +const ( + mybotsCommand = "mybots" + + mybotsStepMenu = "menu" + + mybotsDraftGeneration = "gen" + mybotsDraftPage = "page" + mybotsDraftOptionPrefix = "opt:" + // mybotsDraftReturn marks a botFatherStepValue state that was opened from the + // /mybots menu, so a successful field edit lands back on the Edit Bot menu + // (a fresh message with working buttons) instead of just ending the dialog. + mybotsDraftReturn = "mb_ret" + + // mybotsCallbackDataPrefix tags this menu's callback data. It carries no + // information beyond "this is a @BotFather /mybots button". + mybotsCallbackDataPrefix = "bf:" + mybotsOptionTokenBytes = 6 + mybotsOptionTokenMaxLen = 32 + // mybotsTokenGenerations is how many renders' worth of button tokens stay + // resolvable: the current render plus the two before it, so a second press of + // the same button (or a button on the message just above) still resolves while + // the table stays bounded. + mybotsTokenGenerations = 3 + + // mybotsPageSize bounds one page of the bot list. MaxMarkupButtonsPerRow is 8 + // and one bot is one row, so this stays well inside the keyboard limits with + // room for the navigation row. + mybotsPageSize = 10 +) + +// Opaque button choices as stored in the per-user token table. These strings +// never travel over the wire; only the random token that maps to them does. +const ( + mybotsChoiceListPrefix = "list:" // list: + mybotsChoiceBotPrefix = "bot:" // bot: -> per-bot menu + mybotsChoiceTokenPrefix = "tok:" // tok: -> API token screen + mybotsChoiceRevokePrefix = "rvk:" // rvk: -> revoke confirm + mybotsChoiceRevokeGoPrefix = "rvkgo:" // rvkgo: -> do revoke + mybotsChoiceEditPrefix = "edit:" // edit: -> Edit Bot menu + mybotsChoiceSetNamePrefix = "setname:" // setname: + mybotsChoiceSetAboutPrefix = "setabout:" // setabout: + mybotsChoiceSetDescPrefix = "setdesc:" // setdesc: + mybotsChoiceSetCmdsPrefix = "setcmds:" // setcmds: + mybotsChoiceBotpicPrefix = "botpic:" // botpic: -> phase 2, alert for now + mybotsChoiceCfgPrefix = "cfg:" // cfg: -> Bot Settings screen + mybotsChoiceCfgInline = "cfginl:" // cfginl: -> toggle inline mode + mybotsChoiceCfgGroups = "cfggrp:" // cfggrp: -> toggle allow groups + mybotsChoiceCfgPrivacy = "cfgprv:" // cfgprv: -> toggle group privacy + mybotsChoiceDeletePrefix = "del:" // del: -> delete confirm + mybotsChoiceDeleteGoPrefix = "delgo:" // delgo: -> do delete +) + +const ( + mybotsExpiredButtonText = "That button is no longer active. Send /mybots to open the list again." + mybotsGoneBotText = "That bot is no longer available. Send /mybots to open the list again." + mybotsNoBotsText = "You don't have any bots yet. Use /newbot to create one." +) + +// mybotsOption is one inline button before its token is minted. +type mybotsOption struct { + text string + choice string + style domain.MarkupButtonStyle +} + +// --------------------------------------------------------------------------- +// Entry: /mybots +// --------------------------------------------------------------------------- + +// startMyBots answers /mybots (and the /mybots button path). It sends a fresh +// message carrying the first page of the picker; every later press edits that +// message in place. +func (s *Service) startMyBots(ctx context.Context, userID int64) botReply { + owned, err := s.ownedBots(ctx, userID) + if err != nil { + s.log.Error("botfather: list bots", zap.Int64("user_id", userID), zap.Error(err)) + return internalReply() + } + if len(owned) == 0 { + _ = s.bots.DeleteBotChatState(ctx, domain.BotFatherUserID, userID) + return botReply{Text: mybotsNoBotsText} + } + state := domain.BotChatState{ + BotUserID: domain.BotFatherUserID, + UserID: userID, + Command: mybotsCommand, + Step: mybotsStepMenu, + Draft: map[string]string{}, + } + reply, ok := s.myBotsListScreen(ctx, &state, owned, 0) + if !ok { + return internalReply() + } + if !s.saveMyBotsState(ctx, state) { + return internalReply() + } + return reply +} + +// --------------------------------------------------------------------------- +// Callback entry point +// --------------------------------------------------------------------------- + +// onBotFatherCallback answers one inline-button click on a @BotFather /mybots +// message. It is reached from OnCallbackQuery (verifybot.go). +func (s *Service) onBotFatherCallback(ctx context.Context, query domain.BotCallbackQuery) (domain.BotCallbackAnswer, bool, error) { + userID := query.UserID + mu := s.serviceBotReplyLock(domain.BotFatherUserID, userID) + mu.Lock() + defer mu.Unlock() + + state, found, err := s.bots.GetBotChatState(ctx, domain.BotFatherUserID, userID) + if err != nil { + s.log.Error("botfather: get chat state for callback", zap.Int64("user_id", userID), zap.Error(err)) + return domain.BotCallbackAnswer{}, true, err + } + if !found || state.Command != mybotsCommand { + return mybotsAlert(mybotsExpiredButtonText), true, nil + } + choice, ok := mybotsResolveOption(state, query.Data) + if !ok { + // Callback data absent from this user's own token table: an expired + // generation, or replayed/fabricated data. All refused identically, and + // nothing changes. + return mybotsAlert(mybotsExpiredButtonText), true, nil + } + // The follow-up write is on a context detached from the caller's RPC: the + // answer unblocks the click, and the edit must survive the client hanging up + // straight afterwards. + sendCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second) + defer cancel() + return s.applyMyBotsChoice(sendCtx, state, choice, query), true, nil +} + +// applyMyBotsChoice executes one resolved button choice. Each branch owns its own +// messaging: an in-place edit of query.MessageID for navigation, a fresh message +// when handing off to the text-input flow, or an alert for a refusal. +func (s *Service) applyMyBotsChoice(ctx context.Context, state domain.BotChatState, choice string, query domain.BotCallbackQuery) domain.BotCallbackAnswer { + edit := func(reply botReply) domain.BotCallbackAnswer { + if reply.Text == "" { + return domain.BotCallbackAnswer{} + } + if !s.saveMyBotsState(ctx, state) { + return mybotsAlert(mybotsExpiredButtonText) + } + s.editServiceBotMessage(ctx, domain.BotFatherUserID, state.UserID, query.MessageID, reply) + return domain.BotCallbackAnswer{} + } + + switch { + case strings.HasPrefix(choice, mybotsChoiceListPrefix): + page, _ := strconv.Atoi(strings.TrimPrefix(choice, mybotsChoiceListPrefix)) + owned, err := s.ownedBots(ctx, state.UserID) + if err != nil { + s.log.Error("botfather: list bots", zap.Int64("user_id", state.UserID), zap.Error(err)) + return mybotsAlert(mybotsExpiredButtonText) + } + if len(owned) == 0 { + return edit(s.myBotsEmptyScreen(&state)) + } + reply, ok := s.myBotsListScreen(ctx, &state, owned, page) + if !ok { + return mybotsAlert(mybotsExpiredButtonText) + } + return edit(reply) + + case strings.HasPrefix(choice, mybotsChoiceBotPrefix): + return s.mybotsWithBot(ctx, &state, choice, mybotsChoiceBotPrefix, edit, func(b ownedBot) botReply { + return s.myBotsBotMenu(&state, b) + }) + + case strings.HasPrefix(choice, mybotsChoiceTokenPrefix): + return s.mybotsWithBot(ctx, &state, choice, mybotsChoiceTokenPrefix, edit, func(b ownedBot) botReply { + return s.myBotsTokenScreen(ctx, &state, b, "") + }) + + case strings.HasPrefix(choice, mybotsChoiceRevokePrefix): + return s.mybotsWithBot(ctx, &state, choice, mybotsChoiceRevokePrefix, edit, func(b ownedBot) botReply { + return s.myBotsRevokeConfirm(&state, b) + }) + + case strings.HasPrefix(choice, mybotsChoiceRevokeGoPrefix): + return s.mybotsWithBot(ctx, &state, choice, mybotsChoiceRevokeGoPrefix, edit, func(b ownedBot) botReply { + token, err := s.RevokeBotToken(ctx, state.UserID, b.user.ID) + switch { + case errors.Is(err, domain.ErrBotSessionsNotRevoked): + return s.myBotsTokenScreen(ctx, &state, b, + fmt.Sprintf("Token for @%s changed, but I couldn't cut off sessions that are already logged in - tap Revoke again to be sure.", b.user.Username)) + case err != nil: + s.log.Error("botfather: revoke token", zap.Int64("bot_user_id", b.user.ID), zap.Error(err)) + return internalReply() + } + return s.myBotsTokenScreenWithToken(&state, b, token, + fmt.Sprintf("Token for @%s has been revoked. The old one stops working immediately.", b.user.Username)) + }) + + case strings.HasPrefix(choice, mybotsChoiceEditPrefix): + return s.mybotsWithBot(ctx, &state, choice, mybotsChoiceEditPrefix, edit, func(b ownedBot) botReply { + return s.myBotsEditMenu(ctx, &state, b) + }) + + case strings.HasPrefix(choice, mybotsChoiceCfgPrefix) && !strings.HasPrefix(choice, mybotsChoiceCfgInline) && + !strings.HasPrefix(choice, mybotsChoiceCfgGroups) && !strings.HasPrefix(choice, mybotsChoiceCfgPrivacy): + return s.mybotsWithBot(ctx, &state, choice, mybotsChoiceCfgPrefix, edit, func(b ownedBot) botReply { + return s.myBotsSettingsScreen(&state, b, "") + }) + + case strings.HasPrefix(choice, mybotsChoiceCfgInline): + return s.mybotsToggleSetting(ctx, &state, choice, mybotsChoiceCfgInline, edit, "inline") + case strings.HasPrefix(choice, mybotsChoiceCfgGroups): + return s.mybotsToggleSetting(ctx, &state, choice, mybotsChoiceCfgGroups, edit, "groups") + case strings.HasPrefix(choice, mybotsChoiceCfgPrivacy): + return s.mybotsToggleSetting(ctx, &state, choice, mybotsChoiceCfgPrivacy, edit, "privacy") + + case strings.HasPrefix(choice, mybotsChoiceDeletePrefix): + return s.mybotsWithBot(ctx, &state, choice, mybotsChoiceDeletePrefix, edit, func(b ownedBot) botReply { + return s.myBotsDeleteConfirm(&state, b) + }) + + case strings.HasPrefix(choice, mybotsChoiceDeleteGoPrefix): + return s.mybotsWithBot(ctx, &state, choice, mybotsChoiceDeleteGoPrefix, edit, func(b ownedBot) botReply { + if _, err := s.DeleteBot(ctx, b.user.ID); err != nil { + if errors.Is(err, domain.ErrBotSessionsNotRevoked) { + return botReply{Text: fmt.Sprintf("I couldn't safely delete @%s right now (its sessions are still active). Please try again in a moment.", b.user.Username)} + } + s.log.Error("botfather: delete bot", zap.Int64("bot_user_id", b.user.ID), zap.Error(err)) + return internalReply() + } + owned, err := s.ownedBots(ctx, state.UserID) + if err != nil || len(owned) == 0 { + return s.myBotsEmptyScreen(&state) + } + reply, ok := s.myBotsListScreen(ctx, &state, owned, 0) + if !ok { + return internalReply() + } + reply.Text = fmt.Sprintf("Deleted @%s.\n\n", b.user.Username) + reply.Text + return reply + }) + + case strings.HasPrefix(choice, mybotsChoiceBotpicPrefix): + return s.mybotsBeginValueInput(ctx, state, choice, mybotsChoiceBotpicPrefix, botFatherCmdSetBotpic) + + case strings.HasPrefix(choice, mybotsChoiceSetNamePrefix): + return s.mybotsBeginValueInput(ctx, state, choice, mybotsChoiceSetNamePrefix, botFatherCmdSetName) + case strings.HasPrefix(choice, mybotsChoiceSetAboutPrefix): + return s.mybotsBeginValueInput(ctx, state, choice, mybotsChoiceSetAboutPrefix, botFatherCmdSetAbout) + case strings.HasPrefix(choice, mybotsChoiceSetDescPrefix): + return s.mybotsBeginValueInput(ctx, state, choice, mybotsChoiceSetDescPrefix, botFatherCmdSetDescription) + case strings.HasPrefix(choice, mybotsChoiceSetCmdsPrefix): + return s.mybotsBeginValueInput(ctx, state, choice, mybotsChoiceSetCmdsPrefix, botFatherCmdSetCommands) + + default: + s.log.Warn("botfather: unknown mybots choice", zap.Int64("user_id", state.UserID), zap.String("choice", choice)) + return mybotsAlert(mybotsExpiredButtonText) + } +} + +// mybotsWithBot resolves the bot named by a choice against the caller's owned +// set, then hands it to render. A bot that is not (any longer) theirs falls back +// to the list. +func (s *Service) mybotsWithBot( + ctx context.Context, + state *domain.BotChatState, + choice, prefix string, + edit func(botReply) domain.BotCallbackAnswer, + render func(ownedBot) botReply, +) domain.BotCallbackAnswer { + botID, _ := strconv.ParseInt(strings.TrimPrefix(choice, prefix), 10, 64) + b, ok, err := s.myBotForUser(ctx, state.UserID, botID) + if err != nil { + return mybotsAlert(mybotsExpiredButtonText) + } + if !ok { + owned, listErr := s.ownedBots(ctx, state.UserID) + if listErr != nil || len(owned) == 0 { + return edit(s.myBotsEmptyScreen(state)) + } + reply, built := s.myBotsListScreen(ctx, state, owned, 0) + if !built { + return mybotsAlert(mybotsExpiredButtonText) + } + reply.Text = mybotsGoneBotText + "\n\n" + reply.Text + return edit(reply) + } + return edit(render(b)) +} + +// mybotsToggleSetting flips one of the three per-bot settings and re-renders the +// settings screen with fresh labels. +func (s *Service) mybotsToggleSetting( + ctx context.Context, + state *domain.BotChatState, + choice, prefix string, + edit func(botReply) domain.BotCallbackAnswer, + which string, +) domain.BotCallbackAnswer { + return s.mybotsWithBot(ctx, state, choice, prefix, edit, func(b ownedBot) botReply { + var ( + err error + verb string + ) + switch which { + case "inline": + // "Just inline" per the menu spec: on = a minimal placeholder, off = + // disabled. /setinline still owns the placeholder text. + if b.profile.InlinePlaceholder == "" { + _, err = s.SetInlinePlaceholder(ctx, b.user.ID, "Search") + } else { + _, err = s.SetInlinePlaceholder(ctx, b.user.ID, "") + } + verb = "inline mode" + case "groups": + _, err = s.SetJoinGroups(ctx, b.user.ID, b.profile.Nochats /* was disallowed -> allow */) + verb = "group joining" + case "privacy": + // ChatHistory == privacy OFF. Toggle: enable privacy when it is off. + _, err = s.SetPrivacy(ctx, b.user.ID, b.profile.ChatHistory) + verb = "group privacy" + } + if err != nil { + s.log.Error("botfather: toggle "+verb, zap.Int64("bot_user_id", b.user.ID), zap.Error(err)) + return s.myBotsSettingsScreen(state, b, "That didn't go through. Try again.") + } + fresh, ok, err := s.myBotForUser(ctx, state.UserID, b.user.ID) + if err != nil || !ok { + return s.myBotsSettingsScreen(state, b, "") + } + return s.myBotsSettingsScreen(state, fresh, "Updated "+verb+".") + }) +} + +// mybotsBeginValueInput hands off to the shared text-collection flow used by +// /setname & friends: it writes a botFatherStepValue state for that command and +// sends a fresh prompt message (a value cannot be collected on a button). +func (s *Service) mybotsBeginValueInput(ctx context.Context, state domain.BotChatState, choice, prefix, cmd string) domain.BotCallbackAnswer { + botID, _ := strconv.ParseInt(strings.TrimPrefix(choice, prefix), 10, 64) + b, ok, err := s.myBotForUser(ctx, state.UserID, botID) + if err != nil { + return mybotsAlert(mybotsExpiredButtonText) + } + if !ok { + return mybotsAlert(mybotsGoneBotText) + } + next := domain.BotChatState{ + BotUserID: domain.BotFatherUserID, + UserID: state.UserID, + Command: cmd, + Step: botFatherStepValue, + Draft: map[string]string{ + botFatherDraftBotID: strconv.FormatInt(b.user.ID, 10), + botFatherDraftBotUsername: b.user.Username, + mybotsDraftReturn: "1", + mybotsDraftPage: state.Draft[mybotsDraftPage], + }, + } + if err := s.bots.UpsertBotChatState(ctx, next); err != nil { + s.log.Error("botfather: save mybots value state", zap.Int64("user_id", state.UserID), zap.Error(err)) + return mybotsAlert(mybotsExpiredButtonText) + } + s.sendServiceBotReply(ctx, domain.BotFatherUserID, state.UserID, botReply{ + Text: valuePrompt(cmd, b.user.Username) + "\n\nOr send /cancel.", + }) + return domain.BotCallbackAnswer{} +} + +// --------------------------------------------------------------------------- +// Screens +// --------------------------------------------------------------------------- + +func (s *Service) myBotsEmptyScreen(state *domain.BotChatState) botReply { + // Nothing left to pick: drop the dialog so a stray text does not land on the + // "use the buttons" reminder. + state.Draft = map[string]string{} + return botReply{Text: mybotsNoBotsText} +} + +// myBotsListScreen renders one page of the bot picker and records its buttons. +func (s *Service) myBotsListScreen(ctx context.Context, state *domain.BotChatState, owned []ownedBot, page int) (botReply, bool) { + pages := (len(owned) + mybotsPageSize - 1) / mybotsPageSize + if pages == 0 { + return s.myBotsEmptyScreen(state), true + } + if page < 0 { + page = 0 + } + if page >= pages { + page = pages - 1 + } + start := page * mybotsPageSize + end := start + mybotsPageSize + if end > len(owned) { + end = len(owned) + } + + rows := make([][]mybotsOption, 0, mybotsPageSize+1) + for _, b := range owned[start:end] { + rows = append(rows, []mybotsOption{{ + text: mybotsBotButtonLabel(b), + choice: mybotsChoiceBotPrefix + strconv.FormatInt(b.user.ID, 10), + }}) + } + if pages > 1 { + var nav []mybotsOption + if page > 0 { + nav = append(nav, mybotsOption{text: "‹ Prev", choice: mybotsChoiceListPrefix + strconv.Itoa(page-1)}) + } + if page < pages-1 { + nav = append(nav, mybotsOption{text: "Next ›", choice: mybotsChoiceListPrefix + strconv.Itoa(page+1)}) + } + if len(nav) > 0 { + rows = append(rows, nav) + } + } + + state.Step = mybotsStepMenu + state.Draft[mybotsDraftPage] = strconv.Itoa(page) + markup := s.mybotsKeyboard(state, rows) + text := "Choose a bot from the list below." + if pages > 1 { + text = fmt.Sprintf("Choose a bot from the list below.\n\nPage %d of %d.", page+1, pages) + } + _ = ctx + return botReply{Text: text, ReplyMarkup: markup}, markup != nil +} + +func (s *Service) myBotsBotMenu(state *domain.BotChatState, b ownedBot) botReply { + page := mybotsDraftInt(*state, mybotsDraftPage) + rows := [][]mybotsOption{ + {{text: "API Token", choice: mybotsChoiceTokenPrefix + botID64(b)}}, + {{text: "Edit Bot", choice: mybotsChoiceEditPrefix + botID64(b)}}, + {{text: "Bot Settings", choice: mybotsChoiceCfgPrefix + botID64(b)}}, + {{text: "Delete Bot", choice: mybotsChoiceDeletePrefix + botID64(b), style: domain.MarkupButtonStyleDanger}}, + {{text: "‹ Back to bots", choice: mybotsChoiceListPrefix + strconv.FormatInt(page, 10)}}, + } + markup := s.mybotsKeyboard(state, rows) + return botReply{ + Text: fmt.Sprintf("@%s\n\nWhat do you want to do?", b.user.Username), + ReplyMarkup: markup, + } +} + +func (s *Service) myBotsTokenScreen(ctx context.Context, state *domain.BotChatState, b ownedBot, lead string) botReply { + _ = ctx + profile, found, err := s.bots.GetBot(ctx, b.user.ID) + if err != nil || !found || profile.TokenSecret == "" { + if err != nil { + s.log.Error("botfather: get bot for token", zap.Int64("bot_user_id", b.user.ID), zap.Error(err)) + } + return internalReply() + } + return s.myBotsTokenScreenWithToken(state, b, domain.FormatBotToken(b.user.ID, profile.TokenSecret), lead) +} + +func (s *Service) myBotsTokenScreenWithToken(state *domain.BotChatState, b ownedBot, token, lead string) botReply { + head := lead + if head != "" { + head += "\n\n" + } + head += fmt.Sprintf("Token for @%s:\n", b.user.Username) + reply := tokenReply(head, token, "\n\nKeep it secret - anyone with this token controls the bot.") + rows := [][]mybotsOption{ + {{text: "Revoke current token", choice: mybotsChoiceRevokePrefix + botID64(b), style: domain.MarkupButtonStyleDanger}}, + {{text: "‹ Back", choice: mybotsChoiceBotPrefix + botID64(b)}}, + } + reply.ReplyMarkup = s.mybotsKeyboard(state, rows) + return reply +} + +func (s *Service) myBotsRevokeConfirm(state *domain.BotChatState, b ownedBot) botReply { + rows := [][]mybotsOption{ + {{text: fmt.Sprintf("Yes, revoke @%s's token", b.user.Username), choice: mybotsChoiceRevokeGoPrefix + botID64(b), style: domain.MarkupButtonStyleDanger}}, + {{text: "‹ Keep it", choice: mybotsChoiceTokenPrefix + botID64(b)}}, + } + return botReply{ + Text: fmt.Sprintf("Revoke the current token for @%s?\n\nThe old token stops working immediately and a new one is generated. Anything using the old token will need updating.", b.user.Username), + ReplyMarkup: s.mybotsKeyboard(state, rows), + } +} + +func (s *Service) myBotsEditMenu(ctx context.Context, state *domain.BotChatState, b ownedBot) botReply { + name, about, description, err := s.GetBotInfo(ctx, b.user.ID) + if err != nil { + s.log.Error("botfather: get bot info for edit menu", zap.Int64("bot_user_id", b.user.ID), zap.Error(err)) + return internalReply() + } + commands, err := s.GetBotCommands(ctx, b.user.ID) + if err != nil { + s.log.Error("botfather: get bot commands for edit menu", zap.Int64("bot_user_id", b.user.ID), zap.Error(err)) + return internalReply() + } + page := mybotsDraftInt(*state, mybotsDraftPage) + rows := [][]mybotsOption{ + {{text: "Edit Name", choice: mybotsChoiceSetNamePrefix + botID64(b)}}, + {{text: "Edit About", choice: mybotsChoiceSetAboutPrefix + botID64(b)}}, + {{text: "Edit Description", choice: mybotsChoiceSetDescPrefix + botID64(b)}}, + {{text: "Edit Botpic", choice: mybotsChoiceBotpicPrefix + botID64(b)}}, + {{text: "Edit Commands", choice: mybotsChoiceSetCmdsPrefix + botID64(b)}}, + { + {text: "‹ Back to bot", choice: mybotsChoiceBotPrefix + botID64(b)}, + {text: "‹‹ Bots list", choice: mybotsChoiceListPrefix + strconv.FormatInt(page, 10)}, + }, + } + return botReply{ + Text: myBotsEditSummary(b.user.Username, name, about, description, commands, s.botHasAvatar(ctx, b.user.ID)), + ReplyMarkup: s.mybotsKeyboard(state, rows), + } +} + +// myBotsEditSummary renders the "Edit @bot info" screen: the current value of +// every editable field, mirroring what BotFather shows. +func myBotsEditSummary(username, name, about, description string, commands []domain.BotCommand, hasBotpic bool) string { + orNone := func(v string) string { + v = strings.ReplaceAll(strings.TrimSpace(v), "\n", " ") + if v == "" { + return "🚫" + } + if r := []rune(v); len(r) > 120 { + v = string(r[:117]) + "..." + } + return v + } + cmds := "no commands yet" + if n := len(commands); n == 1 { + cmds = "1 command" + } else if n > 1 { + cmds = fmt.Sprintf("%d commands", n) + } + botpic := "🚫 no botpic" + if hasBotpic { + botpic = "🖼 has a botpic" + } + return fmt.Sprintf( + "Edit @%s info.\n\nName: %s\nAbout: %s\nDescription: %s\nDescription picture: 🚫 no description picture\nBotpic: %s\nCommands: %s\nPrivacy Policy: 🚫", + username, orNone(name), orNone(about), orNone(description), botpic, cmds, + ) +} + +// botHasAvatar reports whether the bot currently has a profile photo. Without a +// file layer wired it answers false. +func (s *Service) botHasAvatar(ctx context.Context, botUserID int64) bool { + if s.botAvatar == nil { + return false + } + ok, err := s.botAvatar.PeerHasAvatar(ctx, domain.PeerTypeUser, botUserID) + if err != nil { + s.log.Warn("botfather: check bot avatar", zap.Int64("bot_user_id", botUserID), zap.Error(err)) + return false + } + return ok +} + +// myBotsReturnToEditMenu rebuilds a fresh /mybots dialog on the Edit Bot menu +// after a field was edited through the shared value-input flow, so the follow-up +// message carries working "Back to bot" / "Bots list" buttons instead of the +// dialog just ending. +func (s *Service) myBotsReturnToEditMenu(ctx context.Context, userID, botID int64, page, lead string) botReply { + b, ok, err := s.myBotForUser(ctx, userID, botID) + if err != nil || !ok { + s.clearState(ctx, userID) + return botReply{Text: strings.TrimSpace(lead)} + } + st := domain.BotChatState{ + BotUserID: domain.BotFatherUserID, + UserID: userID, + Command: mybotsCommand, + Step: mybotsStepMenu, + Draft: map[string]string{}, + } + if p := strings.TrimSpace(page); p != "" { + st.Draft[mybotsDraftPage] = p + } + menu := s.myBotsEditMenu(ctx, &st, b) + if menu.ReplyMarkup == nil || !s.saveMyBotsState(ctx, st) { + s.clearState(ctx, userID) + return botReply{Text: strings.TrimSpace(lead)} + } + if lead = strings.TrimSpace(lead); lead != "" { + menu.Text = lead + "\n\n" + menu.Text + } + return menu +} + +func (s *Service) myBotsSettingsScreen(state *domain.BotChatState, b ownedBot, lead string) botReply { + inlineOn := b.profile.InlinePlaceholder != "" + groupsOn := !b.profile.Nochats + privacyOn := !b.profile.ChatHistory + + rows := [][]mybotsOption{ + {{text: "Inline Mode: " + onOff(inlineOn), choice: mybotsChoiceCfgInline + botID64(b)}}, + {{text: "Allow Groups: " + onOff(groupsOn), choice: mybotsChoiceCfgGroups + botID64(b)}}, + {{text: "Group Privacy: " + onOff(privacyOn), choice: mybotsChoiceCfgPrivacy + botID64(b)}}, + {{text: "‹ Back", choice: mybotsChoiceBotPrefix + botID64(b)}}, + } + body := fmt.Sprintf("Settings for @%s. Tap a row to flip it.\n\n"+ + "- Inline Mode: %s\n"+ + "- Allow Groups: %s (can the bot be added to groups)\n"+ + "- Group Privacy: %s (on = only sees commands and replies in groups)", + b.user.Username, onOff(inlineOn), onOff(groupsOn), onOff(privacyOn)) + if lead != "" { + body = lead + "\n\n" + body + } + return botReply{Text: body, ReplyMarkup: s.mybotsKeyboard(state, rows)} +} + +func (s *Service) myBotsDeleteConfirm(state *domain.BotChatState, b ownedBot) botReply { + rows := [][]mybotsOption{ + {{text: fmt.Sprintf("Yes, delete @%s", b.user.Username), choice: mybotsChoiceDeleteGoPrefix + botID64(b), style: domain.MarkupButtonStyleDanger}}, + {{text: "‹ Cancel", choice: mybotsChoiceBotPrefix + botID64(b)}}, + } + return botReply{ + Text: fmt.Sprintf("Delete @%s for good?\n\nThis cannot be undone. The bot's token stops working, its sessions are cut off, and the username is released.", b.user.Username), + ReplyMarkup: s.mybotsKeyboard(state, rows), + } +} + +// --------------------------------------------------------------------------- +// Lookups +// --------------------------------------------------------------------------- + +// myBotForUser returns the owned bot named by id. ok=false means it is not (any +// longer) one of this user's bots, which is also the authorisation check. +func (s *Service) myBotForUser(ctx context.Context, userID, botID int64) (ownedBot, bool, error) { + if botID <= 0 { + return ownedBot{}, false, nil + } + owned, err := s.ownedBots(ctx, userID) + if err != nil { + s.log.Error("botfather: list bots", zap.Int64("user_id", userID), zap.Error(err)) + return ownedBot{}, false, err + } + for _, b := range owned { + if b.user.ID == botID { + return b, true, nil + } + } + return ownedBot{}, false, nil +} + +// --------------------------------------------------------------------------- +// Button tokens +// --------------------------------------------------------------------------- + +// mybotsKeyboard renders one inline keyboard and records its buttons in the +// per-user token table. Same contract as verifyOptionKeyboard: callback data is +// only ever the prefix plus a random token that keys into *this* user's own +// BotChatState.Draft, tokens are minted per render and kept for +// mybotsTokenGenerations renders. +func (s *Service) mybotsKeyboard(state *domain.BotChatState, rows [][]mybotsOption) *domain.MessageReplyMarkup { + if state == nil || len(rows) == 0 { + return nil + } + if state.Draft == nil { + state.Draft = map[string]string{} + } + generation := mybotsDraftInt(*state, mybotsDraftGeneration) + 1 + state.Draft[mybotsDraftGeneration] = strconv.FormatInt(generation, 10) + mybotsPruneOptions(state, generation) + markup := &domain.MessageReplyMarkup{Type: domain.MessageReplyMarkupInline} + for _, row := range rows { + buttons := make([]domain.MarkupButton, 0, len(row)) + for _, option := range row { + if option.text == "" || option.choice == "" { + continue + } + token := s.mybotsOptionToken() + state.Draft[mybotsDraftOptionPrefix+token] = strconv.FormatInt(generation, 10) + "|" + option.choice + buttons = append(buttons, domain.MarkupButton{ + Type: domain.MarkupButtonCallback, + Text: option.text, + Style: option.style, + Data: []byte(mybotsCallbackDataPrefix + token), + }) + } + if len(buttons) > 0 { + markup.Inline = append(markup.Inline, buttons) + } + } + if len(markup.Inline) == 0 { + return nil + } + return markup +} + +func (s *Service) mybotsOptionToken() string { + var buf [mybotsOptionTokenBytes]byte + if _, err := rand.Read(buf[:]); err == nil { + return hex.EncodeToString(buf[:]) + } + // A crypto/rand failure must not brick the menu. Unpredictability is defence + // in depth: a token is only ever resolved against the caller's own chat state, + // and the RPC edge already requires the data to appear in a keyboard of a + // message in the caller's own box. + return strconv.FormatInt(s.now().UnixNano()+s.replySeq.Add(1), 36) +} + +// mybotsResolveOption maps callback data back onto the choice recorded for it. +// Anything not in this user's own table is refused. +func mybotsResolveOption(state domain.BotChatState, data []byte) (string, bool) { + raw := string(data) + if !strings.HasPrefix(raw, mybotsCallbackDataPrefix) { + return "", false + } + token := raw[len(mybotsCallbackDataPrefix):] + if token == "" || len(token) > mybotsOptionTokenMaxLen { + return "", false + } + value, found := state.Draft[mybotsDraftOptionPrefix+token] + if !found { + return "", false + } + _, choice, split := strings.Cut(value, "|") + if !split || choice == "" { + return "", false + } + return choice, true +} + +func mybotsPruneOptions(state *domain.BotChatState, generation int64) { + oldest := generation - mybotsTokenGenerations + 1 + for key, value := range state.Draft { + if !strings.HasPrefix(key, mybotsDraftOptionPrefix) { + continue + } + rawGen, _, _ := strings.Cut(value, "|") + gen, err := strconv.ParseInt(rawGen, 10, 64) + if err != nil || gen < oldest { + delete(state.Draft, key) + } + } +} + +// --------------------------------------------------------------------------- +// State + small helpers +// --------------------------------------------------------------------------- + +func (s *Service) saveMyBotsState(ctx context.Context, state domain.BotChatState) bool { + state.BotUserID = domain.BotFatherUserID + state.Command = mybotsCommand + if state.Step == "" { + state.Step = mybotsStepMenu + } + clone := domain.BotChatState{ + BotUserID: state.BotUserID, + UserID: state.UserID, + Command: state.Command, + Step: state.Step, + Draft: make(map[string]string, len(state.Draft)), + } + for key, value := range state.Draft { + clone.Draft[key] = value + } + if err := s.bots.UpsertBotChatState(ctx, clone); err != nil { + s.log.Error("botfather: save mybots state", zap.Int64("user_id", state.UserID), zap.Error(err)) + return false + } + return true +} + +func mybotsDraftInt(state domain.BotChatState, key string) int64 { + value, err := strconv.ParseInt(strings.TrimSpace(state.Draft[key]), 10, 64) + if err != nil { + return 0 + } + return value +} + +func mybotsAlert(text string) domain.BotCallbackAnswer { + if len([]rune(text)) > domain.MaxBotCallbackAnswerLen { + text = string([]rune(text)[:domain.MaxBotCallbackAnswerLen]) + } + return domain.BotCallbackAnswer{Alert: true, Message: text} +} + +func botID64(b ownedBot) string { return strconv.FormatInt(b.user.ID, 10) } + +func onOff(on bool) string { + if on { + return "on" + } + return "off" +} + +func mybotsBotButtonLabel(b ownedBot) string { + if b.user.Username != "" { + return "@" + b.user.Username + } + name := strings.TrimSpace(b.user.FirstName) + if name == "" { + name = "bot " + strconv.FormatInt(b.user.ID, 10) + } + return name +} diff --git a/internal/app/bots/mybots_botpic_test.go b/internal/app/bots/mybots_botpic_test.go new file mode 100644 index 00000000..4c283193 --- /dev/null +++ b/internal/app/bots/mybots_botpic_test.go @@ -0,0 +1,128 @@ +package bots + +import ( + "context" + "strings" + "testing" + + "telesrv/internal/domain" + "telesrv/internal/store/memory" +) + +type fakeBotAvatar struct { + ownerType domain.PeerType + ownerID int64 + sourcePhotoID int64 + calls int + err error + hasAvatar bool +} + +func (f *fakeBotAvatar) PeerHasAvatar(_ context.Context, _ domain.PeerType, _ int64) (bool, error) { + return f.hasAvatar, nil +} + +func (f *fakeBotAvatar) SetAvatarFromExistingPhoto(_ context.Context, ownerType domain.PeerType, ownerID, sourcePhotoID int64, _ int) (domain.Photo, error) { + f.calls++ + f.ownerType, f.ownerID, f.sourcePhotoID = ownerType, ownerID, sourcePhotoID + if f.err != nil { + return domain.Photo{}, f.err + } + return domain.Photo{ID: 90210}, nil +} + +func botFatherPhotoMsg(userID, photoID int64) domain.Message { + return domain.Message{ + From: domain.Peer{Type: domain.PeerTypeUser, ID: userID}, + Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.BotFatherUserID}, + Media: &domain.MessageMedia{ + Kind: domain.MessageMediaKindPhoto, + Photo: &domain.Photo{ID: photoID}, + }, + } +} + +func openBotpicPrompt(t *testing.T, svc *Service, messages *memory.MessageStore, owner domain.User) { + t.Helper() + sendToBotFather(t, svc, messages, owner, "/mybots") + pressBotFather(t, svc, messages, owner.ID, "@pic_mb_bot") + pressBotFather(t, svc, messages, owner.ID, "Edit Bot") + _, prompt := pressBotFather(t, svc, messages, owner.ID, "Edit Botpic") + if !strings.Contains(prompt.Body, "profile picture") { + t.Fatalf("botpic prompt = %q", prompt.Body) + } +} + +func TestMyBotsEditBotpicSetsPhoto(t *testing.T) { + users := memory.NewUserStore() + bots := memory.NewBotStore(users) + messages := memory.NewMessageStore(memory.NewDialogStore()) + avatar := &fakeBotAvatar{} + svc := NewService(users, bots, messages, WithBotAvatarStore(avatar)) + owner := newOwner(t, users, "+2100") + created, _, err := svc.CreateBot(context.Background(), owner.ID, "Pic Bot", "pic_mb_bot") + if err != nil { + t.Fatalf("create bot: %v", err) + } + + openBotpicPrompt(t, svc, messages, owner) + + // A non-photo message keeps the step and asks again. + if reply := sendToBotFather(t, svc, messages, owner, "here you go"); !strings.Contains(reply, "send a photo") { + t.Fatalf("text-instead-of-photo reply = %q", reply) + } + if avatar.calls != 0 { + t.Fatalf("avatar setter called for a non-photo message") + } + + svc.respondAsBotFather(owner.ID, botFatherPhotoMsg(owner.ID, 7777)) + reply := botFatherUserReply(t, messages, owner.ID) + if !strings.Contains(reply.Body, "Profile picture updated") { + t.Fatalf("botpic success reply = %q", reply.Body) + } + if avatar.calls != 1 || avatar.ownerID != created.ID || avatar.sourcePhotoID != 7777 || avatar.ownerType != domain.PeerTypeUser { + t.Fatalf("avatar setter got (calls=%d owner=%d photo=%d type=%s)", avatar.calls, avatar.ownerID, avatar.sourcePhotoID, avatar.ownerType) + } + // The dialog is done: a stray message no longer lands on the botpic step. + if reply := sendToBotFather(t, svc, messages, owner, "anything"); strings.Contains(reply, "profile picture") { + t.Fatalf("botpic step still active after success: %q", reply) + } +} + +func TestMyBotsEditBotpicRejectsBadImage(t *testing.T) { + users := memory.NewUserStore() + bots := memory.NewBotStore(users) + messages := memory.NewMessageStore(memory.NewDialogStore()) + avatar := &fakeBotAvatar{err: domain.ErrPhotoInvalid} + svc := NewService(users, bots, messages, WithBotAvatarStore(avatar)) + owner := newOwner(t, users, "+2101") + if _, _, err := svc.CreateBot(context.Background(), owner.ID, "Pic Bot", "pic_mb_bot"); err != nil { + t.Fatalf("create bot: %v", err) + } + + openBotpicPrompt(t, svc, messages, owner) + svc.respondAsBotFather(owner.ID, botFatherPhotoMsg(owner.ID, 7777)) + reply := botFatherUserReply(t, messages, owner.ID) + if !strings.Contains(reply.Body, "couldn't use that image") { + t.Fatalf("bad image reply = %q", reply.Body) + } + // Step is kept so the user can send another photo. + if reply := sendToBotFather(t, svc, messages, owner, "x"); !strings.Contains(reply, "send a photo") { + t.Fatalf("after bad image, step not kept: %q", reply) + } +} + +func TestMyBotsEditBotpicUnsupported(t *testing.T) { + svc, users, _, messages := newTestService(t) // no WithBotAvatarStore + owner := newOwner(t, users, "+2102") + if _, _, err := svc.CreateBot(context.Background(), owner.ID, "Pic Bot", "pic_mb_bot"); err != nil { + t.Fatalf("create bot: %v", err) + } + + openBotpicPrompt(t, svc, messages, owner) + svc.respondAsBotFather(owner.ID, botFatherPhotoMsg(owner.ID, 7777)) + reply := botFatherUserReply(t, messages, owner.ID) + if !strings.Contains(reply.Body, "isn't available on this server") { + t.Fatalf("unsupported reply = %q", reply.Body) + } +} diff --git a/internal/app/bots/mybots_test.go b/internal/app/bots/mybots_test.go new file mode 100644 index 00000000..9da56aba --- /dev/null +++ b/internal/app/bots/mybots_test.go @@ -0,0 +1,475 @@ +package bots + +import ( + "context" + "fmt" + "strings" + "testing" + + "telesrv/internal/domain" + "telesrv/internal/store/memory" +) + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +func botFatherUserReply(t *testing.T, messages *memory.MessageStore, userID int64) domain.Message { + t.Helper() + list, err := messages.ListByUser(context.Background(), userID, domain.MessageFilter{ + HasPeer: true, + Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.BotFatherUserID}, + Limit: 200, + }) + if err != nil { + t.Fatalf("list user history: %v", err) + } + var latest domain.Message + for _, msg := range list.Messages { + if msg.From.ID == domain.BotFatherUserID && msg.ID >= latest.ID { + latest = msg + } + } + if latest.ID == 0 { + t.Fatal("no @BotFather reply in the user's box") + } + if err := domain.ValidateReplyMarkup(latest.ReplyMarkup); err != nil { + t.Fatalf("reply markup invalid: %v (%+v)", err, latest.ReplyMarkup) + } + return latest +} + +// botFatherBotSideMessageID is the id of the bot's own copy of its latest reply +// to userID -- what the RPC edge resolves query.MessageID to before calling the +// responder, and what editServiceBotMessage addresses. +func botFatherBotSideMessageID(t *testing.T, messages *memory.MessageStore, userID int64) int { + t.Helper() + list, err := messages.ListByUser(context.Background(), domain.BotFatherUserID, domain.MessageFilter{ + HasPeer: true, + Peer: domain.Peer{Type: domain.PeerTypeUser, ID: userID}, + Limit: 200, + }) + if err != nil { + t.Fatalf("list bot history: %v", err) + } + latest := 0 + for _, msg := range list.Messages { + if msg.From.ID == domain.BotFatherUserID && msg.ID > latest { + latest = msg.ID + } + } + if latest == 0 { + t.Fatal("no @BotFather copy of its own reply") + } + return latest +} + +func mybotsButtonData(msg domain.Message, label string) ([]byte, bool) { + if msg.ReplyMarkup == nil { + return nil, false + } + for _, row := range msg.ReplyMarkup.Inline { + for _, button := range row { + if button.Type == domain.MarkupButtonCallback && strings.Contains(button.Text, label) { + return append([]byte(nil), button.Data...), true + } + } + } + return nil, false +} + +func mybotsHasButton(msg domain.Message, label string) bool { + _, ok := mybotsButtonData(msg, label) + return ok +} + +func hasMentionEntity(msg domain.Message, mention string) bool { + for _, e := range msg.Entities { + if e.Type == domain.MessageEntityMention && e.Length == len(mention) { + return true + } + } + return false +} + +// pressBotFather clicks the button whose text contains label on the user's latest +// @BotFather reply and returns the callback answer plus the user's new latest +// reply (which, for an in-place edit, is the same message updated). +func pressBotFather(t *testing.T, svc *Service, messages *memory.MessageStore, userID int64, label string) (domain.BotCallbackAnswer, domain.Message) { + t.Helper() + reply := botFatherUserReply(t, messages, userID) + data, ok := mybotsButtonData(reply, label) + if !ok { + t.Fatalf("button %q not in keyboard: %+v", label, reply.ReplyMarkup) + } + return pressBotFatherData(t, svc, messages, userID, data) +} + +func pressBotFatherData(t *testing.T, svc *Service, messages *memory.MessageStore, userID int64, data []byte) (domain.BotCallbackAnswer, domain.Message) { + t.Helper() + answer, handled, err := svc.OnCallbackQuery(context.Background(), domain.BotCallbackQuery{ + ID: 1, + BotUserID: domain.BotFatherUserID, + UserID: userID, + Peer: domain.Peer{Type: domain.PeerTypeUser, ID: userID}, + MessageID: botFatherBotSideMessageID(t, messages, userID), + Data: data, + }) + if err != nil { + t.Fatalf("callback query: %v", err) + } + if !handled { + t.Fatal("callback reported unhandled for @BotFather") + } + return answer, botFatherUserReply(t, messages, userID) +} + +func makeBotsP(t *testing.T, svc *Service, ownerID int64, prefix string, n int) { + t.Helper() + for i := 0; i < n; i++ { + if _, _, err := svc.CreateBot(context.Background(), ownerID, fmt.Sprintf("Bot %d", i), fmt.Sprintf("%s%d_bot", prefix, i)); err != nil { + t.Fatalf("create bot %d: %v", i, err) + } + } +} + +func makeBots(t *testing.T, svc *Service, ownerID int64, n int) { + t.Helper() + makeBotsP(t, svc, ownerID, "mb", n) +} + +// --------------------------------------------------------------------------- +// tests +// --------------------------------------------------------------------------- + +func TestMyBotsPickerPaginates(t *testing.T) { + svc, users, _, messages := newTestService(t) + owner := newOwner(t, users, "+2001") + makeBots(t, svc, owner.ID, mybotsPageSize+3) + + sendToBotFather(t, svc, messages, owner, "/mybots") + page1 := botFatherUserReply(t, messages, owner.ID) + if !strings.Contains(page1.Body, "Page 1 of 2") { + t.Fatalf("page 1 body = %q", page1.Body) + } + if mybotsHasButton(page1, "‹ Prev") { + t.Fatal("first page must not offer Prev") + } + if !mybotsHasButton(page1, "Next ›") { + t.Fatal("first page must offer Next") + } + if !mybotsHasButton(page1, "@mb0_bot") { + t.Fatalf("page 1 missing @mb0_bot: %+v", page1.ReplyMarkup) + } + + _, page2 := pressBotFather(t, svc, messages, owner.ID, "Next ›") + if !strings.Contains(page2.Body, "Page 2 of 2") { + t.Fatalf("page 2 body = %q", page2.Body) + } + if mybotsHasButton(page2, "Next ›") { + t.Fatal("last page must not offer Next") + } + if !mybotsHasButton(page2, "‹ Prev") { + t.Fatal("last page must offer Prev") + } + if !mybotsHasButton(page2, "@mb12_bot") { + t.Fatalf("page 2 missing @mb12_bot: %+v", page2.ReplyMarkup) + } + // The pager edits one message in place rather than piling up new ones. + if page2.ID != page1.ID { + t.Fatalf("pager sent a new message (%d -> %d), want in-place edit", page1.ID, page2.ID) + } +} + +func TestMyBotsBotMenuAndBack(t *testing.T) { + svc, users, _, messages := newTestService(t) + owner := newOwner(t, users, "+2002") + makeBots(t, svc, owner.ID, 2) + + sendToBotFather(t, svc, messages, owner, "/mybots") + _, menu := pressBotFather(t, svc, messages, owner.ID, "@mb0_bot") + if !strings.Contains(menu.Body, "@mb0_bot") { + t.Fatalf("bot menu body = %q", menu.Body) + } + for _, want := range []string{"API Token", "Edit Bot", "Bot Settings", "Delete Bot", "Back to bots"} { + if !mybotsHasButton(menu, want) { + t.Fatalf("bot menu missing %q: %+v", want, menu.ReplyMarkup) + } + } + _, back := pressBotFather(t, svc, messages, owner.ID, "Back to bots") + if !strings.Contains(back.Body, "Choose a bot") { + t.Fatalf("back reply = %q", back.Body) + } +} + +func TestBotFatherStartWithBotOpensItsMenu(t *testing.T) { + svc, users, _, messages := newTestService(t) + owner := newOwner(t, users, "+2011") + makeBots(t, svc, owner.ID, 2) + + // "/start " is the "Manage Bot" deep link: it lands on the per-bot menu. + body := sendToBotFather(t, svc, messages, owner, "/start mb0_bot") + if !strings.Contains(body, "@mb0_bot") || !strings.Contains(body, "What do you want to do?") { + t.Fatalf("/start mb0_bot reply = %q", body) + } + menu := botFatherUserReply(t, messages, owner.ID) + for _, want := range []string{"API Token", "Edit Bot", "Bot Settings", "Delete Bot", "Back to bots"} { + if !mybotsHasButton(menu, want) { + t.Fatalf("start menu missing %q: %+v", want, menu.ReplyMarkup) + } + } + // The buttons are live (state was saved), so Edit Bot works from here. + _, edit := pressBotFather(t, svc, messages, owner.ID, "Edit Bot") + if !strings.Contains(edit.Body, "@mb0_bot") { + t.Fatalf("edit menu after /start = %q", edit.Body) + } + + // A leading @ and an unknown/foreign bot fall back to the greeting. + if body := sendToBotFather(t, svc, messages, owner, "/start @mb1_bot"); !strings.Contains(body, "@mb1_bot") { + t.Fatalf("/start @mb1_bot reply = %q", body) + } + if body := sendToBotFather(t, svc, messages, owner, "/start not_a_real_bot"); !strings.Contains(body, "create a new bot") { + t.Fatalf("/start unknown reply = %q, want greeting", body) + } + if body := sendToBotFather(t, svc, messages, owner, "/start"); !strings.Contains(body, "create a new bot") { + t.Fatalf("bare /start reply = %q, want greeting", body) + } +} + +func TestMyBotsTokenAndRevoke(t *testing.T) { + svc, users, bots, messages := newTestService(t) + owner := newOwner(t, users, "+2003") + created, token, err := svc.CreateBot(context.Background(), owner.ID, "Tok Bot", "tok_mb_bot") + if err != nil { + t.Fatalf("create bot: %v", err) + } + + sendToBotFather(t, svc, messages, owner, "/mybots") + pressBotFather(t, svc, messages, owner.ID, "@tok_mb_bot") + _, tokenScreen := pressBotFather(t, svc, messages, owner.ID, "API Token") + if !strings.Contains(tokenScreen.Body, token) { + t.Fatalf("token screen %q missing token %q", tokenScreen.Body, token) + } + + pressBotFather(t, svc, messages, owner.ID, "Revoke current token") + _, revoked := pressBotFather(t, svc, messages, owner.ID, "Yes, revoke") + if strings.Contains(revoked.Body, token) { + t.Fatalf("revoke screen still shows the old token: %q", revoked.Body) + } + if !strings.Contains(revoked.Body, "has been revoked") { + t.Fatalf("revoke screen = %q", revoked.Body) + } + profile, _, err := bots.GetBot(context.Background(), created.ID) + if err != nil { + t.Fatalf("get bot: %v", err) + } + if domain.FormatBotToken(created.ID, profile.TokenSecret) == token { + t.Fatal("token was not rotated") + } +} + +func TestMyBotsSettingsToggles(t *testing.T) { + svc, users, bots, messages := newTestService(t) + owner := newOwner(t, users, "+2004") + created, _, err := svc.CreateBot(context.Background(), owner.ID, "Cfg Bot", "cfg_mb_bot") + if err != nil { + t.Fatalf("create bot: %v", err) + } + ctx := context.Background() + + sendToBotFather(t, svc, messages, owner, "/mybots") + pressBotFather(t, svc, messages, owner.ID, "@cfg_mb_bot") + _, settings := pressBotFather(t, svc, messages, owner.ID, "Bot Settings") + if !mybotsHasButton(settings, "Inline Mode: off") { + t.Fatalf("settings screen: %+v", settings.ReplyMarkup) + } + + _, afterInline := pressBotFather(t, svc, messages, owner.ID, "Inline Mode:") + if !mybotsHasButton(afterInline, "Inline Mode: on") { + t.Fatalf("inline not toggled on: %+v", afterInline.ReplyMarkup) + } + profile, _, _ := bots.GetBot(ctx, created.ID) + if profile.InlinePlaceholder == "" { + t.Fatal("inline placeholder not set after toggle") + } + + privacyBefore := mybotsHasButton(afterInline, "Group Privacy: on") + _, afterPrivacy := pressBotFather(t, svc, messages, owner.ID, "Group Privacy:") + if mybotsHasButton(afterPrivacy, "Group Privacy: on") == privacyBefore { + t.Fatalf("privacy not toggled: %+v", afterPrivacy.ReplyMarkup) + } + privProfile, _, _ := bots.GetBot(ctx, created.ID) + if (!privProfile.ChatHistory) == privacyBefore { + t.Fatal("privacy flag not flipped in the store") + } + + _, afterGroups := pressBotFather(t, svc, messages, owner.ID, "Allow Groups:") + if !mybotsHasButton(afterGroups, "Allow Groups: off") { + t.Fatalf("groups not toggled off: %+v", afterGroups.ReplyMarkup) + } + profile, _, _ = bots.GetBot(ctx, created.ID) + if !profile.Nochats { + t.Fatal("nochats not set after toggling groups off") + } +} + +func TestMyBotsEditNameHandsOffToValueInput(t *testing.T) { + svc, users, _, messages := newTestService(t) + owner := newOwner(t, users, "+2005") + created, _, err := svc.CreateBot(context.Background(), owner.ID, "Old Name", "edit_mb_bot") + if err != nil { + t.Fatalf("create bot: %v", err) + } + + sendToBotFather(t, svc, messages, owner, "/mybots") + pressBotFather(t, svc, messages, owner.ID, "@edit_mb_bot") + pressBotFather(t, svc, messages, owner.ID, "Edit Bot") + answer, prompt := pressBotFather(t, svc, messages, owner.ID, "Edit Name") + if answer.Message != "" { + t.Fatalf("edit-name click alerted: %q", answer.Message) + } + if !strings.Contains(prompt.Body, "new name") { + t.Fatalf("edit-name prompt = %q", prompt.Body) + } + + reply := sendToBotFather(t, svc, messages, owner, "Shiny New Name") + if !strings.Contains(reply, "Name updated") { + t.Fatalf("set name reply = %q", reply) + } + name, _, _, err := svc.GetBotInfo(context.Background(), created.ID) + if err != nil { + t.Fatalf("get bot info: %v", err) + } + if name != "Shiny New Name" { + t.Fatalf("bot name = %q, want updated", name) + } +} + +// deletableBotStore adds a real DeleteBotAccount (memory.BotStore has none) so +// the /mybots delete flow can be exercised end to end. +type deletableBotStore struct { + *memory.BotStore + deleted map[int64]bool +} + +func (d *deletableBotStore) DeleteBotAccount(ctx context.Context, botUserID int64) (domain.User, error) { + d.deleted[botUserID] = true + return domain.User{ID: botUserID, Bot: true, Deleted: true}, nil +} + +func (d *deletableBotStore) ListBotsByOwner(ctx context.Context, ownerUserID int64) ([]domain.BotProfile, error) { + profiles, err := d.BotStore.ListBotsByOwner(ctx, ownerUserID) + if err != nil { + return nil, err + } + out := profiles[:0] + for _, p := range profiles { + if !d.deleted[p.BotUserID] { + out = append(out, p) + } + } + return out, nil +} + +func TestMyBotsEditBotShowsSummaryAndReturnsAfterEdit(t *testing.T) { + svc, users, _, messages := newTestService(t) + owner := newOwner(t, users, "+2010") + if _, _, err := svc.CreateBot(context.Background(), owner.ID, "Enigma Network", "enigma_mb_bot"); err != nil { + t.Fatalf("create bot: %v", err) + } + + sendToBotFather(t, svc, messages, owner, "/mybots") + pressBotFather(t, svc, messages, owner.ID, "@enigma_mb_bot") + _, edit := pressBotFather(t, svc, messages, owner.ID, "Edit Bot") + for _, want := range []string{"Edit @enigma_mb_bot info.", "Name: Enigma Network", "About: 🚫", "Commands: no commands yet", "Botpic: 🚫"} { + if !strings.Contains(edit.Body, want) { + t.Fatalf("edit summary missing %q:\n%s", want, edit.Body) + } + } + for _, want := range []string{"Edit Name", "Edit About", "Edit Botpic", "Back to bot", "Bots list"} { + if !mybotsHasButton(edit, want) { + t.Fatalf("edit menu missing button %q: %+v", want, edit.ReplyMarkup) + } + } + // The @mention is a tappable entity, not plain text. + if !hasMentionEntity(edit, "@enigma_mb_bot") { + t.Fatalf("no mention entity for @enigma_mb_bot: %+v", edit.Entities) + } + + pressBotFather(t, svc, messages, owner.ID, "Edit About") + sendToBotFather(t, svc, messages, owner, "we host things") + after := botFatherUserReply(t, messages, owner.ID) + if !strings.Contains(after.Body, "About section updated") || !strings.Contains(after.Body, "Edit @enigma_mb_bot info.") { + t.Fatalf("post-edit reply is not the edit menu:\n%s", after.Body) + } + if !strings.Contains(after.Body, "About: we host things") { + t.Fatalf("edit menu did not refresh the About value:\n%s", after.Body) + } + // The bug this guards: pressing another field on the refreshed menu must work, + // not report the button as expired. + answer, prompt := pressBotFather(t, svc, messages, owner.ID, "Edit Botpic") + if answer.Alert { + t.Fatalf("Edit Botpic on the refreshed menu alerted: %q", answer.Message) + } + if !strings.Contains(prompt.Body, "profile picture") { + t.Fatalf("Edit Botpic prompt = %q", prompt.Body) + } +} + +func TestMyBotsDeleteBot(t *testing.T) { + users := memory.NewUserStore() + store := &deletableBotStore{BotStore: memory.NewBotStore(users), deleted: map[int64]bool{}} + messages := memory.NewMessageStore(memory.NewDialogStore()) + svc := NewService(users, store, messages) + svc.SetRouterHooks(&captureRevoker{}) + owner := newOwner(t, users, "+2006") + makeBots(t, svc, owner.ID, 2) + + sendToBotFather(t, svc, messages, owner, "/mybots") + pressBotFather(t, svc, messages, owner.ID, "@mb0_bot") + pressBotFather(t, svc, messages, owner.ID, "Delete Bot") + _, afterDelete := pressBotFather(t, svc, messages, owner.ID, "Yes, delete") + if !strings.Contains(afterDelete.Body, "Deleted @mb0_bot") { + t.Fatalf("after delete body = %q", afterDelete.Body) + } + if mybotsHasButton(afterDelete, "@mb0_bot") { + t.Fatal("deleted bot still listed") + } + if !mybotsHasButton(afterDelete, "@mb1_bot") { + t.Fatalf("surviving bot dropped: %+v", afterDelete.ReplyMarkup) + } +} + +func TestMyBotsForeignAndStaleTokensRefused(t *testing.T) { + svc, users, _, messages := newTestService(t) + alice := newOwner(t, users, "+2007") + bob := newOwner(t, users, "+2008") + makeBotsP(t, svc, alice.ID, "alice", 1) + makeBotsP(t, svc, bob.ID, "bob", 1) + + // Alice opens her menu; Bob replays one of her tokens against his own dialog. + sendToBotFather(t, svc, messages, alice, "/mybots") + aliceMenu := botFatherUserReply(t, messages, alice.ID) + var aliceToken []byte + for _, row := range aliceMenu.ReplyMarkup.Inline { + for _, b := range row { + if len(b.Data) > 0 { + aliceToken = append([]byte(nil), b.Data...) + } + } + } + sendToBotFather(t, svc, messages, bob, "/mybots") + answer, _ := pressBotFatherData(t, svc, messages, bob.ID, aliceToken) + if !answer.Alert || answer.Message == "" { + t.Fatalf("replayed foreign token answer = %+v, want expired-button alert", answer) + } + + // A stale generation of Alice's own buttons is refused too. + sendToBotFather(t, svc, messages, alice, "/mybots") // gen 2 + sendToBotFather(t, svc, messages, alice, "/mybots") // gen 3 + sendToBotFather(t, svc, messages, alice, "/mybots") // gen 4 -> gen 1 pruned + answer, _ = pressBotFatherData(t, svc, messages, alice.ID, aliceToken) + if !answer.Alert { + t.Fatalf("stale token answer = %+v, want expired-button alert", answer) + } +} diff --git a/internal/app/bots/service.go b/internal/app/bots/service.go index f91ce9a0..ac564336 100644 --- a/internal/app/bots/service.go +++ b/internal/app/bots/service.go @@ -6,6 +6,7 @@ package bots import ( "context" "crypto/rand" + "errors" "fmt" "net/url" "strings" @@ -76,6 +77,14 @@ type verificationApplications interface { Application(ctx context.Context, applicationID int64) (domain.VerificationApplication, error) } +// botAvatarStore renders an already-stored photo (one a user sent to @BotFather) +// into a bot's profile photo. app/files.Service satisfies it. It is a narrow port +// because a service bot must not reach into the file layer for anything else. +type botAvatarStore interface { + SetAvatarFromExistingPhoto(ctx context.Context, ownerType domain.PeerType, ownerID, sourcePhotoID int64, date int) (domain.Photo, error) + PeerHasAvatar(ctx context.Context, ownerType domain.PeerType, ownerID int64) (bool, error) +} + // The third-party verification ports live in verifierbot.go // (customVerifications, verifierBotTargets): they are the built-in @verifierbot's // only way to reach the feature, and are kept next to the dialog that uses them. @@ -112,6 +121,7 @@ type Service struct { messages store.MessageStore blocker blockChecker channels publicChannelUsernameResolver + reserved reservedUsernameChecker stickers stickerSetCreator installer userStickerSetInstaller aiChat aiChatGenerator @@ -120,6 +130,7 @@ type Service struct { verifierTargets verifierBotTargets gifCatalog gifCatalogSource telegramLogin *telegramloginapp.Service + botAvatar botAvatarStore hooks RouterHooks textDrafts TextDraftPusher userCache store.UserCache @@ -176,8 +187,34 @@ func WithBlockChecker(c blockChecker) Option { } } +// WithBotAvatarStore injects the ability to set a bot's profile photo from a +// photo a user sent to @BotFather ("Edit Botpic"). Absent it, that action reports +// that it is unavailable on this server. +func WithBotAvatarStore(a botAvatarStore) Option { + return func(s *Service) { + if a != nil { + s.botAvatar = a + } + } +} + // WithPublicChannelUsernameResolver 注入公开频道 username 查询能力,用于 bot // username 预检,避免 bot 与 public channel 产生同名可见入口。 +// reservedUsernameChecker reports whether a name is on the operator blocklist. +type reservedUsernameChecker interface { + IsReserved(ctx context.Context, usernameLower string) (bool, error) +} + +// WithReservedUsernames wires the operator username blocklist so CheckUsername +// reports a reserved bot name as taken instead of available. +func WithReservedUsernames(c reservedUsernameChecker) Option { + return func(s *Service) { + if c != nil { + s.reserved = c + } + } +} + func WithPublicChannelUsernameResolver(c publicChannelUsernameResolver) Option { return func(s *Service) { if c != nil { @@ -512,6 +549,13 @@ func (s *Service) CheckUsername(ctx context.Context, ownerUserID int64, username if !domain.ValidBotUsername(username) { return false, domain.ErrBotUsernameInvalid } + if s.reserved != nil { + if r, err := s.reserved.IsReserved(ctx, strings.ToLower(username)); err != nil { + return false, err + } else if r { + return false, nil + } + } if _, found, err := s.users.ByUsername(ctx, username); err != nil { return false, err } else if found { @@ -902,6 +946,30 @@ func (s *Service) SetPrivacy(ctx context.Context, botUserID int64, enabled bool) return version, nil } +// ErrBotUserpicUnsupported reports that this server has no file layer wired for +// setting a bot's profile photo (WithBotAvatarStore was not supplied). +var ErrBotUserpicUnsupported = errors.New("bot profile photo is not supported by this server") + +// SetBotUserpic makes an already-stored photo (one a user sent to @BotFather) the +// bot's current profile photo. sourcePhotoID is a message photo id; the file +// layer re-renders it into the avatar size set. +func (s *Service) SetBotUserpic(ctx context.Context, botUserID, sourcePhotoID int64) error { + if s == nil || botUserID == 0 { + return domain.ErrBotNotFound + } + if s.botAvatar == nil { + return ErrBotUserpicUnsupported + } + if sourcePhotoID <= 0 { + return domain.ErrPhotoInvalid + } + if _, err := s.botAvatar.SetAvatarFromExistingPhoto(ctx, domain.PeerTypeUser, botUserID, sourcePhotoID, int(s.now().Unix())); err != nil { + return err + } + s.invalidateBotReadCaches(ctx, botUserID) + return nil +} + // CanSendMessage reports whether botUserID has explicit permission to initiate // direct messages with userID. func (s *Service) CanSendMessage(ctx context.Context, userID, botUserID int64) (bool, error) { diff --git a/internal/app/bots/service_bot_entities.go b/internal/app/bots/service_bot_entities.go index ba341157..17be860c 100644 --- a/internal/app/bots/service_bot_entities.go +++ b/internal/app/bots/service_bot_entities.go @@ -45,6 +45,10 @@ func serviceBotReplyEntities(text string, explicit []domain.MessageEntity) []dom offset, length := utf16Range(text, span.start, span.end) appendEntity(domain.MessageEntity{Type: domain.MessageEntityBotCommand, Offset: offset, Length: length}) } + for _, span := range serviceBotMentionByteSpans(text) { + offset, length := utf16Range(text, span.start, span.end) + appendEntity(domain.MessageEntity{Type: domain.MessageEntityMention, Offset: offset, Length: length}) + } sort.SliceStable(out, func(i, j int) bool { if out[i].Offset != out[j].Offset { return out[i].Offset < out[j].Offset @@ -138,6 +142,42 @@ func serviceBotCommandByteSpans(text string) []serviceBotEntitySpan { return spans } +// serviceBotMentionByteSpans finds "@username" runs so a service bot's messages +// render the mention as a tappable link. A username is 5-32 of [A-Za-z0-9_]; the +// "@" must not sit right after a word character (so "you@example" is not a +// mention) and the run must not be followed by one. +func serviceBotMentionByteSpans(text string) []serviceBotEntitySpan { + var spans []serviceBotEntitySpan + for i := 0; i < len(text); { + r, size := utf8.DecodeRuneInString(text[i:]) + if r != '@' || !serviceBotCommandStart(text, i) { + i += size + continue + } + start := i + i += size + nameStart := i + for i < len(text) { + c, csize := utf8.DecodeRuneInString(text[i:]) + if !serviceBotCommandChar(c) { + break + } + i += csize + } + nameLen := i - nameStart + if nameLen < 5 || nameLen > 32 { + continue + } + if i < len(text) { + if next, _ := utf8.DecodeRuneInString(text[i:]); serviceBotCommandChar(next) { + continue + } + } + spans = append(spans, serviceBotEntitySpan{start: start, end: i}) + } + return spans +} + func serviceBotCommandStart(text string, byteIndex int) bool { if byteIndex == 0 { return true diff --git a/internal/app/bots/service_test.go b/internal/app/bots/service_test.go index c68bfc6f..441cae8b 100644 --- a/internal/app/bots/service_test.go +++ b/internal/app/bots/service_test.go @@ -31,10 +31,18 @@ func newOwner(t *testing.T, users *memory.UserStore, phone string) domain.User { // sendToBotFather 同步驱动 responder(绕过 OnPrivateMessage 的 goroutine 派发以 // 保证单测确定性;异步派发由 mtprotoedge bot e2e 覆盖),返回 BotFather 最新回复文本。 +func botFatherMsg(userID int64, text string) domain.Message { + return domain.Message{ + From: domain.Peer{Type: domain.PeerTypeUser, ID: userID}, + Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.BotFatherUserID}, + Body: text, + } +} + func sendToBotFather(t *testing.T, svc *Service, messages *memory.MessageStore, owner domain.User, text string) string { t.Helper() ctx := context.Background() - svc.respondAsBotFather(owner.ID, text) + svc.respondAsBotFather(owner.ID, botFatherMsg(owner.ID, text)) list, err := messages.ListByUser(ctx, owner.ID, domain.MessageFilter{ HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.BotFatherUserID}, @@ -332,8 +340,8 @@ func TestBotFatherMyBotsAndLimit(t *testing.T) { t.Fatalf("create bot %d: %v", i, err) } } - if reply := sendToBotFather(t, svc, messages, owner, "/mybots"); !strings.Contains(reply, "@limit0_bot") { - t.Fatalf("/mybots reply = %q, want bot list", reply) + if reply := sendToBotFather(t, svc, messages, owner, "/mybots"); !strings.Contains(reply, "Choose a bot") { + t.Fatalf("/mybots reply = %q, want bot picker", reply) } if _, _, err := svc.CreateBot(ctx, owner.ID, "One Too Many", "toomany_bot"); err != domain.ErrBotsTooMany { t.Fatalf("create over limit err = %v, want ErrBotsTooMany", err) @@ -384,7 +392,7 @@ func TestBotFatherReplyRespectsBlock(t *testing.T) { owner := newOwner(t, users, "+1099") ctx := context.Background() - svc.respondAsBotFather(owner.ID, "/help") + svc.respondAsBotFather(owner.ID, botFatherMsg(owner.ID, "/help")) // IsBlocked 参数语义:owner(userID) 是否 block 了 BotFather(blockedUserID)。 if blocker.gotUser != owner.ID || blocker.gotPeer != domain.BotFatherUserID { @@ -408,7 +416,7 @@ func TestBotFatherReplyRespectsBlock(t *testing.T) { // 未 block:回复正常投递。 blocker.blocked = false other := newOwner(t, users, "+1098") - svc.respondAsBotFather(other.ID, "/help") + svc.respondAsBotFather(other.ID, botFatherMsg(other.ID, "/help")) otherList, err := messages.ListByUser(ctx, other.ID, domain.MessageFilter{ HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.BotFatherUserID}, diff --git a/internal/app/bots/verifierbot.go b/internal/app/bots/verifierbot.go index d12c637a..96d855d6 100644 --- a/internal/app/bots/verifierbot.go +++ b/internal/app/bots/verifierbot.go @@ -194,13 +194,13 @@ const ( // verifierBotWhatText is the part of /start that is true whether or not an // operator has activated this bot, so it is said first and unconditionally. -const verifierBotWhatText = `I hand out THIRD-PARTY verification. +var verifierBotWhatText = `I hand out THIRD-PARTY verification. A third-party mark is a verifier's own icon, shown right before the name of a bot, a channel or an account, plus one line of description in its profile. It means "this verifier vouches for this peer" -- nothing more. -It is NOT the official ` + branding.ProductName + ` checkmark. The platform badge is granted by the platform itself (@verifybot collects those applications); a third-party mark is granted by the company running a verifier bot. The two are stored, shown and taken away separately, and neither one implies the other.` +It is NOT the official ` + branding.ProductName() + ` checkmark. The platform badge is granted by the platform itself (@verifybot collects those applications); a third-party mark is granted by the company running a verifier bot. The two are stored, shown and taken away separately, and neither one implies the other.` -const verifierBotHelpText = `I am a verifier bot. I grant third-party marks: my icon before the name of your bot, channel or account, plus a description in its profile. This is not the official ` + branding.ProductName + ` checkmark. +var verifierBotHelpText = `I am a verifier bot. I grant third-party marks: my icon before the name of your bot, channel or account, plus a description in its profile. This is not the official ` + branding.ProductName() + ` checkmark. /start - what a third-party mark is and who grants it /verify - apply for the mark @@ -1121,7 +1121,7 @@ func verifierSummaryText(state domain.BotChatState, settings domain.BotVerifierS b.WriteString("\n\nWhy:\n") b.WriteString(state.Draft[verifierDraftReason]) b.WriteString("\n\nThis is a third-party mark, not the official ") - b.WriteString(branding.ProductName) + b.WriteString(branding.ProductName()) b.WriteString(" checkmark, and I do not decide: an operator reads the application and either grants the mark or refuses it. I will message you here either way.") return b.String() } @@ -1170,7 +1170,7 @@ func verifierDecisionText(req domain.CustomVerificationRequest) (string, bool) { switch req.Status { case domain.CustomVerificationApproved: return fmt.Sprintf("Application #%d is approved: %s now carries my mark -- my icon before the name and my description in the profile.\n\nThis is a third-party mark, not the official %s checkmark. Send /revoke if you ever want it removed.", - req.ID, label, branding.ProductName), true + req.ID, label, branding.ProductName()), true case domain.CustomVerificationRejected: text := fmt.Sprintf("Application #%d for %s was not approved, so no mark was granted.", req.ID, label) if reason := strings.TrimSpace(req.DecisionReason); reason != "" { diff --git a/internal/app/bots/verifybot.go b/internal/app/bots/verifybot.go index 26c17e95..a6bb95cc 100644 --- a/internal/app/bots/verifybot.go +++ b/internal/app/bots/verifybot.go @@ -114,7 +114,7 @@ const ( verifyChoiceBlockedPrefix = "no:" ) -const verifyBotStartText = `I collect applications for official ` + branding.ProductName + ` verification: the badge shown next to the name of a channel, supergroup or bot whose identity has been confirmed. +var verifyBotStartText = `I collect applications for official ` + branding.ProductName() + ` verification: the badge shown next to the name of a channel, supergroup or bot whose identity has been confirmed. Before you apply, check that the subject of the application: - is a channel, supergroup or bot with a public @username; @@ -126,7 +126,7 @@ This badge is never sold and never granted automatically. A person reads every a Tap the button below, or send /new, to start. Send /help for the full list of commands.` -const verifyBotHelpText = `I collect official ` + branding.ProductName + ` verification applications. +var verifyBotHelpText = `I collect official ` + branding.ProductName() + ` verification applications. /new - file a verification application /status - list your applications and their status @@ -355,6 +355,10 @@ func (s *Service) OnCallbackQuery(ctx context.Context, query domain.BotCallbackQ // (verifierbot.go). return s.onVerifierCallback(ctx, query) } + if query.BotUserID == domain.BotFatherUserID { + // @BotFather's button-driven /mybots menu (mybots.go). + return s.onBotFatherCallback(ctx, query) + } if query.BotUserID != domain.VerifyBotUserID { // The other built-in bots never attach an inline keyboard, so there is // nothing to route. An empty answer still beats hanging the click for the diff --git a/internal/app/bots/verifybot_test.go b/internal/app/bots/verifybot_test.go index 6fc68350..b34d08fd 100644 --- a/internal/app/bots/verifybot_test.go +++ b/internal/app/bots/verifybot_test.go @@ -869,12 +869,13 @@ func TestVerifyBotCallbackForForeignBotIsNotClaimed(t *testing.T) { }); handled || err != nil { t.Fatalf("foreign bot callback handled=%v err=%v, want (false, nil)", handled, err) } - // A built-in bot with no keyboards is claimed but answered empty, so the click - // cannot hang for the whole callback timeout. + // @BotFather owns the /mybots menu now: a click with no active dialog is + // claimed and answered with the expired-button alert, so it cannot hang for + // the whole callback timeout. answer, handled, err := svc.OnCallbackQuery(context.Background(), domain.BotCallbackQuery{ - BotUserID: domain.BotFatherUserID, UserID: 900, Data: []byte("x"), + BotUserID: domain.BotFatherUserID, UserID: 900, Data: []byte("bf:x"), }) - if !handled || err != nil || answer.Message != "" { + if !handled || err != nil || !answer.Alert || answer.Message == "" { t.Fatalf("BotFather callback = (%+v, %v, %v)", answer, handled, err) } } diff --git a/internal/app/botverification/seed.go b/internal/app/botverification/seed.go index ad9839a7..5ed2936f 100644 --- a/internal/app/botverification/seed.go +++ b/internal/app/botverification/seed.go @@ -42,7 +42,7 @@ func (s *Service) SeedDefaultVerifier(ctx context.Context) (bool, error) { if _, err := s.GrantVerifier(ctx, domain.BotVerifierSettings{ BotID: domain.VerifierBotUserID, IconDocumentID: icon.DocumentID, - CompanyName: branding.ProductName, + CompanyName: branding.ProductName(), DefaultDescription: "Bundled reference verifier -- auto-granted on first boot.", CanModifyCustomDescription: false, Enabled: true, diff --git a/internal/app/channels/participants_cache.go b/internal/app/channels/participants_cache.go index 6a7fc820..629963c8 100644 --- a/internal/app/channels/participants_cache.go +++ b/internal/app/channels/participants_cache.go @@ -64,9 +64,6 @@ func (c *participantsReadModelCache) invalidateChannel(channelID int64) { func (s *Service) cachedParticipants(ctx context.Context, userID, channelID int64, filter domain.ChannelParticipantsFilter, offset, limit int) (domain.ChannelParticipantList, error) { filter, offset, limit = normalizeParticipantsRequest(filter, offset, limit) - if s.participantCache == nil || s.versions == nil { - return s.loadParticipants(ctx, userID, channelID, filter, offset, limit) - } key := participantsCacheKey{ userID: userID, channelID: channelID, @@ -75,15 +72,23 @@ func (s *Service) cachedParticipants(ctx context.Context, userID, channelID int6 offset: offset, limit: limit, } + if s.participantCache == nil || s.versions == nil { + return s.loadParticipantsWithContentHash(ctx, userID, channelID, filter, key) + } hash, err := s.channelParticipantsHash(ctx, userID, channelID, key) if err != nil { return domain.ChannelParticipantList{}, err } if hash == 0 { - return s.loadParticipants(ctx, userID, channelID, filter, offset, limit) + // The read-model version hash is unavailable (e.g. a channel whose + // read_model_versions rows were never seeded). Fall back to a stable + // content hash so the RPC layer can still answer + // channels.channelParticipantsNotModified. Without a non-zero, stable + // Hash a client that polls the member list re-fetches it forever. + return s.loadParticipantsWithContentHash(ctx, userID, channelID, filter, key) } return s.participantCache.getOrLoad(ctx, key, hash, func() (domain.ChannelParticipantList, error) { - list, err := s.loadParticipants(ctx, userID, channelID, filter, offset, limit) + list, err := s.loadParticipants(ctx, userID, channelID, filter, key.offset, key.limit) if err != nil { return domain.ChannelParticipantList{}, err } @@ -92,6 +97,54 @@ func (s *Service) cachedParticipants(ctx context.Context, userID, channelID int6 }) } +// loadParticipantsWithContentHash loads a participants page and, when nothing has +// assigned an opaque version hash, derives a deterministic one from the page's +// own contents so identical results keep producing an identical Hash. +func (s *Service) loadParticipantsWithContentHash(ctx context.Context, userID, channelID int64, filter domain.ChannelParticipantsFilter, key participantsCacheKey) (domain.ChannelParticipantList, error) { + list, err := s.loadParticipants(ctx, userID, channelID, filter, key.offset, key.limit) + if err != nil { + return domain.ChannelParticipantList{}, err + } + if list.Hash == 0 { + list.Hash = participantsContentHash(channelID, key, list) + } + return list, nil +} + +// participantsContentHash is a stable fingerprint of a participants page: the +// channel, the page key and every returned member's client-visible identity +// (id, role, rank, status). Any change a client would render (a new member, a +// promotion, a rank edit, a kick) changes the hash; an unchanged page does not. +func participantsContentHash(channelID int64, key participantsCacheKey, list domain.ChannelParticipantList) int64 { + h := fnv.New64a() + var buf [8]byte + writeUint := func(v uint64) { + binary.LittleEndian.PutUint64(buf[:], v) + _, _ = h.Write(buf[:]) + } + writeStr := func(s string) { + _, _ = h.Write([]byte(s)) + _, _ = h.Write([]byte{0}) + } + writeUint(uint64(channelID)) + writeStr(string(key.kind)) + writeStr(key.query) + writeUint(uint64(key.offset)) + writeUint(uint64(key.limit)) + writeUint(uint64(int64(list.Count))) + for _, p := range list.Participants { + writeUint(uint64(p.UserID)) + writeStr(string(p.Role)) + writeStr(string(p.Status)) + writeStr(p.Rank) + } + sum := int64(h.Sum64() & 0x7fffffffffffffff) + if sum == 0 { + return 1 + } + return sum +} + func (s *Service) loadParticipants(ctx context.Context, userID, channelID int64, filter domain.ChannelParticipantsFilter, offset, limit int) (domain.ChannelParticipantList, error) { if filter.Kind == domain.ChannelParticipantsBots && s.bots != nil { return s.getBotParticipants(ctx, userID, channelID, offset, limit) diff --git a/internal/app/channels/service_test.go b/internal/app/channels/service_test.go index a8e7f908..12cd65cc 100644 --- a/internal/app/channels/service_test.go +++ b/internal/app/channels/service_test.go @@ -814,6 +814,51 @@ func TestGetParticipantsCacheInvalidatesAfterAdminMutation(t *testing.T) { } } +func TestGetParticipantsFallsBackToContentHashWithoutReadModelVersions(t *testing.T) { + ctx := context.Background() + const ownerID int64 = 1001 + base := &countingChannelStore{ChannelStore: memory.NewChannelStore()} + // No WithReadModelVersions: channelParticipantsHash can never build an opaque + // version hash, so the service must derive a stable one from the page itself. + service := NewService(base) + created, err := service.CreateChannel(ctx, ownerID, domain.CreateChannelRequest{ + Title: "Fallback Hash", + Megagroup: true, + MemberUserIDs: []int64{1002}, + Date: 1700004105, + }) + if err != nil { + t.Fatalf("CreateChannel: %v", err) + } + filter := domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsRecent} + + first, err := service.GetParticipants(ctx, ownerID, created.Channel.ID, filter, 0, 20) + if err != nil { + t.Fatalf("first participants: %v", err) + } + if first.Hash == 0 { + t.Fatalf("first participants hash = 0, want stable non-zero fallback") + } + second, err := service.GetParticipants(ctx, ownerID, created.Channel.ID, filter, 0, 20) + if err != nil { + t.Fatalf("second participants: %v", err) + } + if second.Hash != first.Hash { + t.Fatalf("second hash = %d, want stable %d", second.Hash, first.Hash) + } + + if _, err := service.InviteToChannel(ctx, ownerID, created.Channel.ID, []int64{1003}, 1700004106); err != nil { + t.Fatalf("InviteToChannel: %v", err) + } + third, err := service.GetParticipants(ctx, ownerID, created.Channel.ID, filter, 0, 20) + if err != nil { + t.Fatalf("third participants: %v", err) + } + if third.Hash == first.Hash { + t.Fatalf("third hash = %d, want changed after a new member joined", third.Hash) + } +} + func TestFullMegagroupAdminGrantFillsManageRanks(t *testing.T) { ctx := context.Background() service := NewService(memory.NewChannelStore()) @@ -2716,6 +2761,61 @@ func TestChannelUsernameAndSignatures(t *testing.T) { } } +func TestUpdateUsernameForcesPreHistoryVisible(t *testing.T) { + ctx := context.Background() + const ownerID int64 = 1001 + service := NewService(memory.NewChannelStore()) + created, err := service.CreateMegagroupFromCreateChat(ctx, ownerID, domain.CreateChannelRequest{ + Title: "Private First", + MemberUserIDs: []int64{1002}, + Date: 10, + }) + if err != nil { + t.Fatalf("CreateMegagroupFromCreateChat: %v", err) + } + + hidden, err := service.SetPreHistoryHidden(ctx, ownerID, created.Channel.ID, true) + if err != nil { + t.Fatalf("SetPreHistoryHidden: %v", err) + } + if !hidden.PreHistoryHidden { + t.Fatalf("hidden channel = %+v, want pre-history hidden", hidden) + } + + // Assigning a public username must force pre-history back to visible. + public, err := service.UpdateUsername(ctx, ownerID, domain.UpdateChannelUsernameRequest{ + ChannelID: created.Channel.ID, + Username: "private_first_pub", + }) + if err != nil { + t.Fatalf("UpdateUsername: %v", err) + } + if public.PreHistoryHidden { + t.Fatalf("public channel = %+v, want pre-history visible after publish", public) + } + + // Removing the username leaves the flag alone (still visible). + private, err := service.UpdateUsername(ctx, ownerID, domain.UpdateChannelUsernameRequest{ + ChannelID: created.Channel.ID, + Username: "", + }) + if err != nil { + t.Fatalf("UpdateUsername clear: %v", err) + } + if private.PreHistoryHidden { + t.Fatalf("re-privated channel = %+v, want pre-history still visible", private) + } + + // ...and the creator can hide it again once private. + rehidden, err := service.SetPreHistoryHidden(ctx, ownerID, created.Channel.ID, true) + if err != nil { + t.Fatalf("SetPreHistoryHidden after re-privating: %v", err) + } + if !rehidden.PreHistoryHidden { + t.Fatalf("re-hidden channel = %+v, want pre-history hidden again", rehidden) + } +} + func TestListStoryPostableChannelsFiltersPostStoryRights(t *testing.T) { ctx := context.Background() service := NewService(memory.NewChannelStore()) diff --git a/internal/app/files/bot_avatar.go b/internal/app/files/bot_avatar.go new file mode 100644 index 00000000..19519464 --- /dev/null +++ b/internal/app/files/bot_avatar.go @@ -0,0 +1,84 @@ +package files + +import ( + "context" + "fmt" + "time" + + "go.uber.org/zap" + + "telesrv/internal/domain" +) + +// maxBotAvatarSourceBytes bounds the source image @BotFather turns into a bot's +// profile photo. A profile photo a user sends in a chat is a server-rendered +// message-size JPEG, comfortably under this. +const maxBotAvatarSourceBytes = 12 << 20 + +// SetAvatarFromExistingPhoto renders an already-stored photo (typically one a +// user just sent to a service bot) into the s/a/c avatar size set and makes it +// the current profile photo of ownerType/ownerID. +// +// It is the path @BotFather's "Edit Botpic" uses: photos.uploadProfilePhoto#bot +// is not accepted, and the source is a message photo rather than a fresh upload, +// so the ordinary UploadProfilePhoto path does not apply. +func (s *Service) SetAvatarFromExistingPhoto(ctx context.Context, ownerType domain.PeerType, ownerID, sourcePhotoID int64, date int) (domain.Photo, error) { + if ownerID <= 0 || sourcePhotoID <= 0 { + return domain.Photo{}, domain.ErrPhotoInvalid + } + source, found, err := s.media.GetPhoto(ctx, sourcePhotoID) + if err != nil { + return domain.Photo{}, err + } + if !found { + return domain.Photo{}, domain.ErrPhotoInvalid + } + data, ok := s.photoSourceBytes(ctx, source) + if !ok || !s.ValidateAvatarUpload(data) { + return domain.Photo{}, domain.ErrPhotoInvalid + } + if date == 0 { + date = int(time.Now().Unix()) + } + photo, err := s.createAvatarPhoto(ctx, data, ownerID) + if err != nil { + return domain.Photo{}, err + } + if err := s.media.AddProfilePhotoKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile, photo.ID, date); err != nil { + return domain.Photo{}, err + } + return photo, nil +} + +// PeerHasAvatar reports whether ownerType/ownerID has a current profile photo. +func (s *Service) PeerHasAvatar(ctx context.Context, ownerType domain.PeerType, ownerID int64) (bool, error) { + _, ok, err := s.CurrentProfilePhotoKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile) + return ok, err +} + +// photoSourceBytes reads the original image bytes behind a stored photo. Every +// static size of a photo written by putPhotoStaticSizes points at the same +// stored object, so any one size yields the full image. +func (s *Service) photoSourceBytes(ctx context.Context, photo domain.Photo) ([]byte, bool) { + for _, size := range photo.Sizes { + if size.Type == "" { + continue + } + key := fmt.Sprintf("photo:%d:%s", photo.ID, size.Type) + blob, found, err := s.media.GetFileBlob(ctx, key) + if err != nil || !found || blob.Size <= 0 || blob.Size > maxBotAvatarSourceBytes { + continue + } + backend, err := s.backendFor(blob.Backend) + if err != nil { + continue + } + body, total, err := backend.GetRange(ctx, blob.ObjectKey, 0, blob.Size) + if err != nil || total != blob.Size || int64(len(body)) != blob.Size { + s.log.Warn("read bot avatar source blob failed", zap.String("location_key", key), zap.Error(err)) + continue + } + return body, true + } + return nil, false +} diff --git a/internal/app/files/sticker_creator.go b/internal/app/files/sticker_creator.go index eeff4fff..3d98e616 100644 --- a/internal/app/files/sticker_creator.go +++ b/internal/app/files/sticker_creator.go @@ -304,6 +304,10 @@ func (s *Service) prepareStickerSetDocument(ctx context.Context, doc domain.Docu func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Document) (domain.Document, error) { mimeType := canonicalStickerMaterialMime(doc.StickerSetMaterialMime()) + // A stored documentAttributeImageSize with a zero dimension is worse than a + // missing one: clients divide by it and crash. Strip any such attribute here + // so the branches below re-derive a real 512x512 (or decoded) size. + doc.Attributes = dropZeroImageSizeAttributes(doc.Attributes) hasImageSize := false hasVideo := false for _, attr := range doc.Attributes { @@ -311,7 +315,9 @@ func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Doc case domain.DocAttrImageSize: hasImageSize = true case domain.DocAttrVideo: - hasVideo = true + if attr.W > 0 && attr.H > 0 { + hasVideo = true + } } } switch mimeType { @@ -444,6 +450,20 @@ func (s *Service) rewriteStickerMaterialBlob(ctx context.Context, docID int64, d return nil } +// dropZeroImageSizeAttributes removes documentAttributeImageSize entries whose +// width or height is not positive. Such an attribute reaches clients as +// documentAttributeImageSize#0 and is divided by while sizing the render. +func dropZeroImageSizeAttributes(attrs []domain.DocumentAttribute) []domain.DocumentAttribute { + out := attrs[:0:0] + for _, a := range attrs { + if a.Kind == domain.DocAttrImageSize && (a.W <= 0 || a.H <= 0) { + continue + } + out = append(out, a) + } + return out +} + func replaceStickerMaterialFilename(attrs []domain.DocumentAttribute, fallback string) []domain.DocumentAttribute { out := append([]domain.DocumentAttribute(nil), attrs...) for i := range out { diff --git a/internal/app/langpack/service.go b/internal/app/langpack/service.go index 2514fe88..feb1a56c 100644 --- a/internal/app/langpack/service.go +++ b/internal/app/langpack/service.go @@ -54,7 +54,7 @@ func newServiceWithCacheLimits(packs store.LangPackStore, maxBytes int64, maxEnt packs: packs, packCache: newLangPackCache(maxBytes, maxEntries), languageCache: newLanguageListCache(languageEntries), - publicBaseURL: branding.DefaultPublicURL, + publicBaseURL: branding.PublicBaseURL(), } } diff --git a/internal/app/passkey/service.go b/internal/app/passkey/service.go index bb25262a..f4e5f736 100644 --- a/internal/app/passkey/service.go +++ b/internal/app/passkey/service.go @@ -71,7 +71,7 @@ func NewService(creds store.PasskeyStore, challenges store.PasskeyChallengeStore creds: creds, challenges: challenges, rpID: rpID, - rpName: branding.ProductName, + rpName: branding.ProductName(), dcID: dcID, challengeTTL: defaultChallengeTTL, now: time.Now, diff --git a/internal/app/users/service.go b/internal/app/users/service.go index 2fc78932..f745c175 100644 --- a/internal/app/users/service.go +++ b/internal/app/users/service.go @@ -769,7 +769,16 @@ func (s *Service) putCachedUsers(ctx context.Context, users ...domain.User) { // Collectible ownership may change inside the star-gift aggregate. Keep // these uncommon users on the authoritative store path so the database // lifecycle trigger can never be masked by a stale base-user cache entry. - if user.ID != 0 && user.EmojiStatusCollectible.Empty() { + // + // redisstore.userBaseValue has no field for Deleted/DeletedAt/Status: caching + // a deleted user silently resets Deleted to false (and Status to the zero + // UserStatusUnknown) on every round trip, which never self-heals -- each + // subsequent miss reloads the correctly tombstoned DB row and immediately + // re-corrupts it on write. That regressed a deleted account back to looking + // live (blank name, but "last seen recently" instead of "Deleted Account"). + // Keep deleted users off the cache so lookups always hit the authoritative + // store, which encodes the tombstone correctly. + if user.ID != 0 && user.EmojiStatusCollectible.Empty() && !user.Deleted { cacheable = append(cacheable, user) } } diff --git a/internal/app/users/service_test.go b/internal/app/users/service_test.go index a3029039..553d2293 100644 --- a/internal/app/users/service_test.go +++ b/internal/app/users/service_test.go @@ -397,6 +397,69 @@ func TestServiceUsesBaseCacheWithoutCachingViewerOverlay(t *testing.T) { } } +// deletedOverrideUserStore reports one user id as an already-tombstoned +// domain.User regardless of what the underlying memory store holds, so the +// test doesn't need a memory.UserStore deletion helper to exercise the +// base-user cache's handling of deleted users. +type deletedOverrideUserStore struct { + *memory.UserStore + deletedID int64 + byIDsCalls int +} + +func (s *deletedOverrideUserStore) ByIDs(ctx context.Context, ids []int64) ([]domain.User, error) { + s.byIDsCalls++ + users, err := s.UserStore.ByIDs(ctx, ids) + if err != nil { + return nil, err + } + for i, u := range users { + if u.ID == s.deletedID { + users[i] = domain.User{ID: u.ID, Deleted: true, Status: domain.UserStatus{Kind: domain.UserStatusEmpty}} + } + } + return users, nil +} + +// TestServiceNeverCachesDeletedUser guards the redisstore.UserCache cache +// schema gap: userBaseValue carries no Deleted/DeletedAt/Status field, so +// caching a deleted user silently resets Deleted to false (and Status to the +// zero UserStatusUnknown) on every round trip -- a bug that never self-heals, +// since each subsequent cache miss reloads the correctly tombstoned row and +// immediately re-corrupts it on write. It regressed a deleted account back to +// looking live: blank name (still blank, that part survives), but "last seen +// recently" instead of "Deleted Account". The service must keep deleted users +// off the base cache entirely so every lookup hits the authoritative store. +func TestServiceNeverCachesDeletedUser(t *testing.T) { + ctx := context.Background() + base := memory.NewUserStore() + owner, err := base.Create(ctx, domain.User{AccessHash: 1, Phone: "15550000031", FirstName: "Owner"}) + if err != nil { + t.Fatalf("create owner: %v", err) + } + target, err := base.Create(ctx, domain.User{AccessHash: 2, Phone: "15550000032", FirstName: "Target"}) + if err != nil { + t.Fatalf("create target: %v", err) + } + store := &deletedOverrideUserStore{UserStore: base, deletedID: target.ID} + cache := newMemoryBaseUserCache() + svc := NewService(store, WithBaseUserCache(cache)) + + got, found, err := svc.ByID(ctx, owner.ID, target.ID) + if err != nil || !found || !got.Deleted { + t.Fatalf("ByID = %+v found=%v err=%v, want a deleted user", got, found, err) + } + if _, cached := cache.users[target.ID]; cached { + t.Fatalf("cache holds deleted user %d, want it kept off the cache entirely", target.ID) + } + if _, found, err := svc.ByID(ctx, owner.ID, target.ID); err != nil || !found { + t.Fatalf("second ByID found=%v err=%v", found, err) + } + if store.byIDsCalls != 2 { + t.Fatalf("store ByIDs calls = %d, want 2 (deleted user must never be served from cache)", store.byIDsCalls) + } +} + func TestServiceRefreshesBaseCacheAfterProfileUpdate(t *testing.T) { ctx := context.Background() base := memory.NewUserStore() diff --git a/internal/botapi/projection.go b/internal/botapi/projection.go index 6c4c10ca..ddfc39f6 100644 --- a/internal/botapi/projection.go +++ b/internal/botapi/projection.go @@ -356,6 +356,163 @@ func apiMediaUsesCaption(media map[string]any) bool { return false } +// apiChatFull projects a getChat result. The bot-api chat-id encoding is applied +// here: users keep their positive id, channels/supergroups become +// -1000000000000 - channelID. +func apiChatFull(chat domain.BotAPIChat) map[string]any { + id := chat.Peer.ID + if chat.Peer.Type == domain.PeerTypeChannel { + id = -1000000000000 - chat.Peer.ID + } + out := map[string]any{"id": id, "type": chat.Type} + if chat.Title != "" { + out["title"] = chat.Title + } + if chat.Username != "" { + out["username"] = chat.Username + } + if chat.FirstName != "" { + out["first_name"] = chat.FirstName + } + if chat.LastName != "" { + out["last_name"] = chat.LastName + } + if chat.Description != "" { + out["description"] = chat.Description + } + if chat.IsForum { + out["is_forum"] = true + } + if chat.Verified { + out["is_verified"] = true + } + if chat.Scam { + out["is_scam"] = true + } + if chat.Fake { + out["is_fake"] = true + } + if chat.SlowModeDelay > 0 { + out["slow_mode_delay"] = chat.SlowModeDelay + } + if chat.LinkedChatID != 0 { + out["linked_chat_id"] = -1000000000000 - chat.LinkedChatID + } + if chat.Permissions != nil { + out["permissions"] = apiChatPermissions(*chat.Permissions) + } + if chat.PinnedMessage != nil { + out["pinned_message"] = apiMessage(*chat.PinnedMessage, chat.PinnedMessageUsers) + } + return out +} + +// apiChatPermissions projects a channel's default restrictions as a Bot API +// ChatPermissions object (a right is granted when the matching restriction is +// off). +func apiChatPermissions(b domain.ChannelBannedRights) map[string]any { + text := !b.SendMessages && !b.SendPlain + return map[string]any{ + "can_send_messages": text, + "can_send_audios": !b.SendMedia && !b.SendAudios, + "can_send_documents": !b.SendMedia && !b.SendDocs, + "can_send_photos": !b.SendMedia && !b.SendPhotos, + "can_send_videos": !b.SendMedia && !b.SendVideos, + "can_send_video_notes": !b.SendMedia && !b.SendRoundvideos, + "can_send_voice_notes": !b.SendMedia && !b.SendVoices, + "can_send_polls": !b.SendPolls, + "can_send_other_messages": !b.SendStickers && !b.SendGifs && !b.SendGames && !b.SendInline, + "can_add_web_page_previews": !b.EmbedLinks, + "can_change_info": !b.ChangeInfo, + "can_invite_users": !b.InviteUsers, + "can_pin_messages": !b.PinMessages, + "can_manage_topics": !b.ManageTopics, + } +} + +// apiChatMember projects a resolved member as a Bot API ChatMember object. +func apiChatMember(m domain.BotAPIChatMember) map[string]any { + out := map[string]any{ + "status": botAPIMemberStatus(m.Member), + "user": apiUser(userOrPlaceholder(m.User, m.Member.UserID)), + } + switch out["status"] { + case "creator": + if m.Member.AdminRights.Anonymous { + out["is_anonymous"] = true + } + if m.Member.Rank != "" { + out["custom_title"] = m.Member.Rank + } + case "administrator": + a := m.Member.AdminRights + out["can_be_edited"] = false + out["is_anonymous"] = a.Anonymous + out["can_manage_chat"] = a.ManageChat + out["can_delete_messages"] = a.DeleteMessages + out["can_manage_video_chats"] = a.ManageCall + out["can_restrict_members"] = a.BanUsers + out["can_promote_members"] = a.AddAdmins + out["can_change_info"] = a.ChangeInfo + out["can_invite_users"] = a.InviteUsers + out["can_post_messages"] = a.PostMessages + out["can_edit_messages"] = a.EditMessages + out["can_pin_messages"] = a.PinMessages + out["can_manage_topics"] = a.ManageTopics + out["can_post_stories"] = a.PostStories + out["can_edit_stories"] = a.EditStories + out["can_delete_stories"] = a.DeleteStories + if m.Member.Rank != "" { + out["custom_title"] = m.Member.Rank + } + case "restricted": + b := m.Member.BannedRights + out["is_member"] = m.Member.Status == domain.ChannelMemberActive + for k, v := range apiChatPermissions(b) { + out[k] = v + } + if b.UntilDate > 0 { + out["until_date"] = b.UntilDate + } + case "kicked": + if m.Member.BannedRights.UntilDate > 0 { + out["until_date"] = m.Member.BannedRights.UntilDate + } + } + return out +} + +func userOrPlaceholder(u domain.User, id int64) domain.User { + if u.ID != 0 { + return u + } + return domain.User{ID: id} +} + +func botAPIMemberStatus(m domain.ChannelMember) string { + switch { + case m.Role == domain.ChannelRoleCreator: + return "creator" + case m.Status == domain.ChannelMemberKicked, m.Status == domain.ChannelMemberBanned, m.BannedRights.ViewMessages: + return "kicked" + case m.Role == domain.ChannelRoleAdmin: + return "administrator" + case m.Status == domain.ChannelMemberLeft: + return "left" + case botAPIMemberRestricted(m.BannedRights): + return "restricted" + default: + return "member" + } +} + +func botAPIMemberRestricted(b domain.ChannelBannedRights) bool { + return b.SendMessages || b.SendMedia || b.SendStickers || b.SendGifs || b.SendGames || + b.SendInline || b.EmbedLinks || b.SendPolls || b.ChangeInfo || b.InviteUsers || + b.PinMessages || b.ManageTopics || b.SendPhotos || b.SendVideos || b.SendRoundvideos || + b.SendAudios || b.SendVoices || b.SendDocs || b.SendPlain || b.SendReactions +} + func apiChat(peer domain.Peer, users map[int64]domain.User) map[string]any { switch peer.Type { case domain.PeerTypeUser: diff --git a/internal/botapi/server.go b/internal/botapi/server.go index 7bfcef76..19a40719 100644 --- a/internal/botapi/server.go +++ b/internal/botapi/server.go @@ -41,6 +41,9 @@ type WebAppService interface { type GatewayService interface { BotAPISelf(ctx context.Context, botID int64) (domain.User, error) + BotAPIChat(ctx context.Context, botID, chatID int64) (domain.BotAPIChat, error) + BotAPIChatMemberCount(ctx context.Context, botID, chatID int64) (int, error) + BotAPIChatMember(ctx context.Context, botID, chatID, userID int64) (domain.BotAPIChatMember, error) BotAPIUpdates(ctx context.Context, botID int64, offset int64) ([]domain.UpdateEvent, error) BotAPISendMessage(ctx context.Context, botID, chatID int64, text string, entities []domain.MessageEntity, replyMarkup *domain.MessageReplyMarkup, disableWebPagePreview, silent bool, replyToMessageID int) (domain.Message, error) BotAPISendRichMessage(ctx context.Context, botID, chatID int64, rich domain.BotAPIRichMessageInput, replyMarkup *domain.MessageReplyMarkup, silent, noForwards bool, replyToMessageID int, effectID int64) (domain.Message, error) @@ -197,6 +200,12 @@ func (h *handler) handle(w http.ResponseWriter, r *http.Request) { switch strings.ToLower(method) { case "getme": h.getMe(w, r, botID) + case "getchat": + h.getChat(w, r, botID) + case "getchatmembercount", "getchatmemberscount": + h.getChatMemberCount(w, r, botID) + case "getchatmember": + h.getChatMember(w, r, botID) case "setmycommands": h.setMyCommands(w, r, botID) case "deletemycommands": @@ -310,6 +319,81 @@ func (h *handler) getMe(w http.ResponseWriter, r *http.Request, botID int64) { writeAPIOK(w, apiUser(u)) } +func (h *handler) getChat(w http.ResponseWriter, r *http.Request, botID int64) { + if h.gateway == nil { + writeAPIError(w, http.StatusNotImplemented, "METHOD_NOT_FOUND") + return + } + values, err := requestValues(r) + if err != nil { + writeAPIError(w, http.StatusBadRequest, "BAD_REQUEST") + return + } + // Numeric chat_id only - no @username resolution. + chatID, err := strconv.ParseInt(strings.TrimSpace(values["chat_id"]), 10, 64) + if err != nil || chatID == 0 { + writeAPIError(w, http.StatusBadRequest, "CHAT_ID_INVALID") + return + } + chat, err := h.gateway.BotAPIChat(r.Context(), botID, chatID) + if err != nil { + writeAPIError(w, http.StatusBadRequest, apiErrorDescription(err)) + return + } + writeAPIOK(w, apiChatFull(chat)) +} + +func (h *handler) getChatMemberCount(w http.ResponseWriter, r *http.Request, botID int64) { + if h.gateway == nil { + writeAPIError(w, http.StatusNotImplemented, "METHOD_NOT_FOUND") + return + } + values, err := requestValues(r) + if err != nil { + writeAPIError(w, http.StatusBadRequest, "BAD_REQUEST") + return + } + chatID, err := strconv.ParseInt(strings.TrimSpace(values["chat_id"]), 10, 64) + if err != nil || chatID == 0 { + writeAPIError(w, http.StatusBadRequest, "CHAT_ID_INVALID") + return + } + count, err := h.gateway.BotAPIChatMemberCount(r.Context(), botID, chatID) + if err != nil { + writeAPIError(w, http.StatusBadRequest, apiErrorDescription(err)) + return + } + writeAPIOK(w, count) +} + +func (h *handler) getChatMember(w http.ResponseWriter, r *http.Request, botID int64) { + if h.gateway == nil { + writeAPIError(w, http.StatusNotImplemented, "METHOD_NOT_FOUND") + return + } + values, err := requestValues(r) + if err != nil { + writeAPIError(w, http.StatusBadRequest, "BAD_REQUEST") + return + } + chatID, err := strconv.ParseInt(strings.TrimSpace(values["chat_id"]), 10, 64) + if err != nil || chatID == 0 { + writeAPIError(w, http.StatusBadRequest, "CHAT_ID_INVALID") + return + } + userID, err := strconv.ParseInt(strings.TrimSpace(values["user_id"]), 10, 64) + if err != nil || userID <= 0 { + writeAPIError(w, http.StatusBadRequest, "USER_ID_INVALID") + return + } + member, err := h.gateway.BotAPIChatMember(r.Context(), botID, chatID, userID) + if err != nil { + writeAPIError(w, http.StatusBadRequest, apiErrorDescription(err)) + return + } + writeAPIOK(w, apiChatMember(member)) +} + func (h *handler) getUpdates(w http.ResponseWriter, r *http.Request, botID int64) { if h.gateway == nil { writeAPIError(w, http.StatusNotImplemented, "METHOD_NOT_FOUND") @@ -1457,6 +1541,7 @@ func apiErrorDescription(err error) string { "BUTTON_URL_INVALID", "BOT_INVALID", "CHAT_ID_INVALID", + "CHAT_NOT_FOUND", "ENTITY_INVALID", "ENTITIES_TOO_LONG", "ENTITY_BOUNDS_INVALID", diff --git a/internal/botapi/server_test.go b/internal/botapi/server_test.go index b4b36fdc..60600c0a 100644 --- a/internal/botapi/server_test.go +++ b/internal/botapi/server_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "mime/multipart" "net/http" @@ -161,6 +162,111 @@ func TestGetMeUsesGateway(t *testing.T) { } } +func TestGetChatUsesGateway(t *testing.T) { + bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}} + gateway := &fakeBotAPIGateway{ + chat: domain.BotAPIChat{ + Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 42}, + Type: "supergroup", + Title: "Test", + Username: "test1", + Description: "a test group", + IsForum: true, + }, + } + h := (&handler{bots: bots, gateway: gateway}).routes() + + rec := performBotAPIRequest(t, h, bots.profile, "getChat", `{"chat_id":-1000000000042}`) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String()) + } + if gateway.chatChatID != -1000000000042 { + t.Fatalf("gateway chat_id = %d, want -1000000000042", gateway.chatChatID) + } + var resp struct { + OK bool `json:"ok"` + Result struct { + ID int64 `json:"id"` + Type string `json:"type"` + Title string `json:"title"` + Username string `json:"username"` + Description string `json:"description"` + IsForum bool `json:"is_forum"` + } `json:"result"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode: %v", err) + } + if !resp.OK || resp.Result.ID != -1000000000042 || resp.Result.Type != "supergroup" || + resp.Result.Username != "test1" || resp.Result.Description != "a test group" || !resp.Result.IsForum { + t.Fatalf("response = %s", rec.Body.String()) + } + + // A private chat the bot cannot see comes back as chat not found. + gateway.chatErr = errors.New("CHAT_NOT_FOUND") + rec = performBotAPIRequest(t, h, bots.profile, "getChat", `{"chat_id":-1000000000099}`) + if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "CHAT_NOT_FOUND") { + t.Fatalf("not-found response status=%d body=%s", rec.Code, rec.Body.String()) + } + + // @username is rejected before it reaches the gateway. + rec = performBotAPIRequest(t, h, bots.profile, "getChat", `{"chat_id":"@test1"}`) + if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "CHAT_ID_INVALID") { + t.Fatalf("username response status=%d body=%s", rec.Code, rec.Body.String()) + } +} + +func TestGetChatMemberCountAndMember(t *testing.T) { + bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}} + gateway := &fakeBotAPIGateway{ + memberCount: 7, + member: domain.BotAPIChatMember{ + User: domain.User{ID: 500, FirstName: "Ann"}, + Member: domain.ChannelMember{ + UserID: 500, Role: domain.ChannelRoleAdmin, Status: domain.ChannelMemberActive, + AdminRights: domain.ChannelAdminRights{BanUsers: true, PinMessages: true}, + Rank: "mod", + }, + }, + } + h := (&handler{bots: bots, gateway: gateway}).routes() + + rec := performBotAPIRequest(t, h, bots.profile, "getChatMemberCount", `{"chat_id":-1000000000042}`) + if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"result":7`) { + t.Fatalf("getChatMemberCount status=%d body=%s", rec.Code, rec.Body.String()) + } + + rec = performBotAPIRequest(t, h, bots.profile, "getChatMember", `{"chat_id":-1000000000042,"user_id":500}`) + if rec.Code != http.StatusOK { + t.Fatalf("getChatMember status=%d body=%s", rec.Code, rec.Body.String()) + } + var resp struct { + OK bool `json:"ok"` + Result struct { + Status string `json:"status"` + CustomTitle string `json:"custom_title"` + CanRestrict bool `json:"can_restrict_members"` + CanPromote bool `json:"can_promote_members"` + User struct { + ID int64 `json:"id"` + } `json:"user"` + } `json:"result"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode: %v", err) + } + if !resp.OK || resp.Result.Status != "administrator" || resp.Result.User.ID != 500 || + resp.Result.CustomTitle != "mod" || !resp.Result.CanRestrict || resp.Result.CanPromote { + t.Fatalf("getChatMember result = %s", rec.Body.String()) + } + + // user_id is required. + rec = performBotAPIRequest(t, h, bots.profile, "getChatMember", `{"chat_id":-1000000000042}`) + if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "USER_ID_INVALID") { + t.Fatalf("missing user_id status=%d body=%s", rec.Code, rec.Body.String()) + } +} + func TestBotCommandsPreserveEphemeralFlag(t *testing.T) { bots := &fakeBotAPIBots{profile: domain.BotProfile{BotUserID: 1001, TokenSecret: "secret"}} h := (&handler{bots: bots}).routes() @@ -1442,6 +1548,13 @@ func (f *fakeWebAppService) SavePreparedInlineMessageFromBotAPI(_ context.Contex type fakeBotAPIGateway struct { self domain.User + chat domain.BotAPIChat + chatErr error + chatChatID int64 + memberCount int + member domain.BotAPIChatMember + memberErr error + updates []domain.UpdateEvent updateBotID int64 updateOffset int64 @@ -1502,6 +1615,19 @@ func (f *fakeBotAPIGateway) BotAPISelf(context.Context, int64) (domain.User, err return f.self, nil } +func (f *fakeBotAPIGateway) BotAPIChat(_ context.Context, _ int64, chatID int64) (domain.BotAPIChat, error) { + f.chatChatID = chatID + return f.chat, f.chatErr +} + +func (f *fakeBotAPIGateway) BotAPIChatMemberCount(context.Context, int64, int64) (int, error) { + return f.memberCount, f.memberErr +} + +func (f *fakeBotAPIGateway) BotAPIChatMember(context.Context, int64, int64, int64) (domain.BotAPIChatMember, error) { + return f.member, f.memberErr +} + func (f *fakeBotAPIGateway) BotAPIUpdates(_ context.Context, botID int64, offset int64) ([]domain.UpdateEvent, error) { f.updateBotID = botID f.updateOffset = offset diff --git a/internal/branding/branding.go b/internal/branding/branding.go index 0a08920d..ea88d8e3 100644 --- a/internal/branding/branding.go +++ b/internal/branding/branding.go @@ -6,44 +6,133 @@ package branding import ( + "fmt" "net/url" "regexp" "strings" + "sync/atomic" + "unicode" + + "telesrv/internal/links" ) -const ( - ProductName = "OwpenGram" - ProductUsername = "owpengram" - DesktopAppName = "OwpenGram Desktop" - AndroidAppName = "OwpenGram Android" - IOSAppName = "OwpenGram iOS" - MacOSAppName = "OwpenGram macOS" - WebAAppName = "OwpenGram Web A" - WebKAppName = "OwpenGram Web K" - PremiumName = "OwpenGram Premium" - StarsName = "OwpenGram Stars" - DefaultPublicURL = "https://owpengram.org" +// Config is the deployment-wide, user-visible product identity. It is loaded +// once during process startup; protocol identifiers and client detection +// tokens deliberately remain outside this structure. +type Config struct { + ProductName string + ProductUsername string + DesktopAppName string + AndroidAppName string + IOSAppName string + MacOSAppName string + WebAAppName string + WebKAppName string + PremiumName string + StarsName string + PublicBaseURL string +} + +var ( + defaultConfig = Config{ + ProductName: "OwpenGram", + ProductUsername: "owpengram", + DesktopAppName: "OwpenGram Desktop", + AndroidAppName: "OwpenGram Android", + IOSAppName: "OwpenGram iOS", + MacOSAppName: "OwpenGram macOS", + WebAAppName: "OwpenGram Web A", + WebKAppName: "OwpenGram Web K", + PremiumName: "OwpenGram Premium", + StarsName: "OwpenGram Stars", + PublicBaseURL: links.DefaultDownloadURL, + } + configured atomic.Pointer[Config] ) +// DefaultConfig returns a copy of the default product identity. +func DefaultConfig() Config { return defaultConfig } + +// Validate normalizes and validates a product identity without installing it. +func Validate(cfg Config) (Config, error) { + for _, field := range []struct { + name string + value *string + }{ + {name: "product name", value: &cfg.ProductName}, + {name: "desktop app name", value: &cfg.DesktopAppName}, + {name: "Android app name", value: &cfg.AndroidAppName}, + {name: "iOS app name", value: &cfg.IOSAppName}, + {name: "macOS app name", value: &cfg.MacOSAppName}, + {name: "Web A app name", value: &cfg.WebAAppName}, + {name: "Web K app name", value: &cfg.WebKAppName}, + {name: "Premium name", value: &cfg.PremiumName}, + {name: "Stars name", value: &cfg.StarsName}, + } { + normalized, err := validateDisplayName(*field.value) + if err != nil { + return Config{}, fmt.Errorf("%s: %w", field.name, err) + } + *field.value = normalized + } + cfg.ProductUsername = strings.TrimPrefix(strings.TrimSpace(cfg.ProductUsername), "@") + if !validProductUsername(cfg.ProductUsername) { + return Config{}, fmt.Errorf("product username must be 5-32 ASCII username characters and start with a letter") + } + cfg.ProductUsername = strings.ToLower(cfg.ProductUsername) + var err error + cfg.PublicBaseURL, err = links.ValidateBaseURL(cfg.PublicBaseURL) + if err != nil { + return Config{}, fmt.Errorf("public base URL: %w", err) + } + return cfg, nil +} + +// Configure installs the validated process-wide identity before services are +// constructed. Readers only ever observe complete immutable snapshots. +func Configure(cfg Config) error { + normalized, err := Validate(cfg) + if err != nil { + return err + } + configured.Store(&normalized) + return nil +} + +// Current returns a copy of the installed product identity. +func Current() Config { + if cfg := configured.Load(); cfg != nil { + return *cfg + } + return defaultConfig +} + +func ProductName() string { return Current().ProductName } +func ProductUsername() string { return Current().ProductUsername } +func PremiumName() string { return Current().PremiumName } +func StarsName() string { return Current().StarsName } +func PublicBaseURL() string { return Current().PublicBaseURL } + // ClientAppName returns the branded display name for a stored client platform. // Stored detection tokens remain unchanged; this is only used at presentation // boundaries such as account.getAuthorizations. func ClientAppName(platform string) string { + cfg := Current() switch strings.ToLower(strings.TrimSpace(platform)) { case "android": - return AndroidAppName + return cfg.AndroidAppName case "ios": - return IOSAppName + return cfg.IOSAppName case "macos": - return MacOSAppName + return cfg.MacOSAppName case "telegram-tt", "weba": - return WebAAppName + return cfg.WebAAppName case "tweb", "webk": - return WebKAppName + return cfg.WebKAppName case "tdesktop", "desktop", "windows": - return DesktopAppName + return cfg.DesktopAppName default: - return ProductName + return cfg.ProductName } } @@ -78,18 +167,49 @@ func UserVisibleText(value, publicBaseURL string) string { if technicalIDRE.MatchString(value) { return value } - return officialBrandRE.ReplaceAllString(value, ProductName) + return officialBrandRE.ReplaceAllString(value, ProductName()) } func publicDestination(raw string) (string, string) { raw = strings.TrimRight(strings.TrimSpace(raw), "/") if raw == "" { - raw = DefaultPublicURL + raw = PublicBaseURL() } parsed, err := url.Parse(raw) if err != nil || parsed.Scheme == "" || parsed.Hostname() == "" { - raw = DefaultPublicURL + raw = PublicBaseURL() parsed, _ = url.Parse(raw) } return raw, parsed.Host } + +func validateDisplayName(raw string) (string, error) { + name := strings.TrimSpace(raw) + if name == "" { + return "", fmt.Errorf("must not be empty") + } + if len([]rune(name)) > 64 { + return "", fmt.Errorf("must not exceed 64 characters") + } + for _, r := range name { + if unicode.IsControl(r) { + return "", fmt.Errorf("must not contain control characters") + } + } + return name, nil +} + +func validProductUsername(username string) bool { + if len(username) < 5 || len(username) > 32 { + return false + } + for i, r := range username { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z': + case i > 0 && (r >= '0' && r <= '9' || r == '_'): + default: + return false + } + } + return true +} diff --git a/internal/branding/branding_test.go b/internal/branding/branding_test.go index a4606635..58086aa0 100644 --- a/internal/branding/branding_test.go +++ b/internal/branding/branding_test.go @@ -46,13 +46,14 @@ func TestUserVisibleTextRebrandsLocalizedProductNames(t *testing.T) { } func TestClientPresentationNames(t *testing.T) { + cfg := Current() for platform, want := range map[string]string{ - "tdesktop": DesktopAppName, - "android": AndroidAppName, - "ios": IOSAppName, - "macos": MacOSAppName, - "telegram-tt": WebAAppName, - "tweb": WebKAppName, + "tdesktop": cfg.DesktopAppName, + "android": cfg.AndroidAppName, + "ios": cfg.IOSAppName, + "macos": cfg.MacOSAppName, + "telegram-tt": cfg.WebAAppName, + "tweb": cfg.WebKAppName, } { if got := ClientAppName(platform); got != want { t.Fatalf("ClientAppName(%q) = %q, want %q", platform, got, want) @@ -62,3 +63,55 @@ func TestClientPresentationNames(t *testing.T) { t.Fatalf("UserVisibleClientPlatform() = %q, want weba", got) } } + +func TestConfigureInstallsCompleteBrandSnapshot(t *testing.T) { + previous := Current() + t.Cleanup(func() { + if err := Configure(previous); err != nil { + t.Fatalf("restore branding: %v", err) + } + }) + + cfg := Config{ + ProductName: "Example Chat", + ProductUsername: "@Example_Chat", + DesktopAppName: "Example Workstation", + AndroidAppName: "Example Droid", + IOSAppName: "Example Phone", + MacOSAppName: "Example Mac", + WebAAppName: "Example Web Alpha", + WebKAppName: "Example Web Kappa", + PremiumName: "Example Plus", + StarsName: "Example Credits", + PublicBaseURL: "https://links.example.test/root/", + } + if err := Configure(cfg); err != nil { + t.Fatalf("Configure: %v", err) + } + if got := Current(); got.ProductUsername != "example_chat" || got.PublicBaseURL != "https://links.example.test/root" { + t.Fatalf("Current() = %+v", got) + } + if got := ClientAppName("android"); got != "Example Droid" { + t.Fatalf("ClientAppName(android) = %q", got) + } + if got := UserVisibleText("Telegram at t.me/example", ""); got != "Example Chat at links.example.test/example" { + t.Fatalf("UserVisibleText() = %q", got) + } +} + +func TestValidateRejectsIncompleteOrUnsafeBranding(t *testing.T) { + for name, mutate := range map[string]func(*Config){ + "blank product": func(cfg *Config) { cfg.ProductName = " " }, + "control": func(cfg *Config) { cfg.StarsName = "bad\nname" }, + "username": func(cfg *Config) { cfg.ProductUsername = "3bad" }, + "public URL": func(cfg *Config) { cfg.PublicBaseURL = "file:///tmp/brand" }, + } { + t.Run(name, func(t *testing.T) { + cfg := DefaultConfig() + mutate(&cfg) + if _, err := Validate(cfg); err == nil { + t.Fatal("Validate accepted invalid branding") + } + }) + } +} diff --git a/internal/domain/account.go b/internal/domain/account.go index 77369673..7ca459e2 100644 --- a/internal/domain/account.go +++ b/internal/domain/account.go @@ -7,6 +7,7 @@ import ( var ( ErrPasswordHashInvalid = errors.New("password hash invalid") + ErrPasswordMissing = errors.New("password missing") ErrSRPIDInvalid = errors.New("srp id invalid") ErrSRPPasswordChanged = errors.New("srp password changed") ErrNewSettingsInvalid = errors.New("new password settings invalid") diff --git a/internal/domain/botapi_chat.go b/internal/domain/botapi_chat.go new file mode 100644 index 00000000..5b2172d9 --- /dev/null +++ b/internal/domain/botapi_chat.go @@ -0,0 +1,32 @@ +package domain + +// BotAPIChat is a peer resolved for the Bot API getChat method. The bot need +// not be a member: a public channel or supergroup resolves (projected as a +// preview), while a private chat the bot has no access to resolves to an error. +type BotAPIChat struct { + Peer Peer // domain peer; the Bot API chat-id encoding is applied by the projection + Type string // "private" | "group" | "supergroup" | "channel" + Title string + Username string + FirstName string + LastName string + Description string // channel/supergroup "about" + IsForum bool + Verified bool + Scam bool + Fake bool + + // Channel/supergroup only, from the full view. + SlowModeDelay int + LinkedChatID int64 // domain channel id; the projection applies the Bot API encoding + Permissions *ChannelBannedRights // default restrictions; nil for a user chat + PinnedMessage *Message + PinnedMessageUsers []User +} + +// BotAPIChatMember is a resolved chat member for the Bot API getChatMember +// method. +type BotAPIChatMember struct { + User User + Member ChannelMember +} diff --git a/internal/domain/channel.go b/internal/domain/channel.go index c531c048..6fc9fed9 100644 --- a/internal/domain/channel.go +++ b/internal/domain/channel.go @@ -714,6 +714,23 @@ type ChannelMessage struct { Deleted bool } +// ForumReplyTopicID resolves the topic a reply to target belongs to inside a +// forum. Every forum message lives in exactly one topic, and a reply inherits +// the target's topic - never the target's own id. Using target.ID is +// discussion-thread logic (comment threads on a broadcast post) and does not +// apply to forums: it manufactures a topic reference that no channel_forum_topics +// row backs, which strict clients cannot place. A target with no recorded topic +// is in General. +func ForumReplyTopicID(target ChannelMessage) int { + if target.Action != nil && target.Action.Type == ChannelActionTopicCreate { + return target.ID // the target itself is a topic root + } + if target.ReplyTo != nil && target.ReplyTo.TopMessageID > 0 { + return target.ReplyTo.TopMessageID + } + return ForumGeneralTopicID +} + // ProjectChannelHistoryClearMessage returns the owner-local service-message // projection for one channel history boundary. Identity fields from the shared // source are retained when available, while all user payload, media, reply, diff --git a/internal/domain/login_code_delivery.go b/internal/domain/login_code_delivery.go index 6f4ff134..bed9b41d 100644 --- a/internal/domain/login_code_delivery.go +++ b/internal/domain/login_code_delivery.go @@ -8,9 +8,9 @@ import ( "telesrv/internal/branding" ) -const officialLoginCodeMessageTemplate = `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName + `! +var officialLoginCodeMessageTemplate = `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName() + `! -This code can be used to log in to your ` + branding.ProductName + ` account. We never ask it for anything else. +This code can be used to log in to your ` + branding.ProductName() + ` account. We never ask it for anything else. If you didn't request this code by trying to log in on another device, simply ignore this message.` diff --git a/internal/domain/reserved_username.go b/internal/domain/reserved_username.go new file mode 100644 index 00000000..69f6b13d --- /dev/null +++ b/internal/domain/reserved_username.go @@ -0,0 +1,24 @@ +package domain + +import "time" + +// MaxReservedUsernameReasonLength bounds the operator note on a reservation. +const MaxReservedUsernameReasonLength = 512 + +// ReservedUsername is one entry in the operator username blocklist. A reserved +// name cannot be taken as an editable username by any peer and cannot be minted +// as a collectible. +type ReservedUsername struct { + Username string // display form (original case at reservation time) + Reason string + Actor string + CreatedAt time.Time +} + +// ReservedUsernameFilter pages the blocklist. Query matches a username prefix +// (case-insensitive); an empty query lists everything. +type ReservedUsernameFilter struct { + Query string + Limit int + Offset int +} diff --git a/internal/domain/system.go b/internal/domain/system.go index 30468fce..7537afcb 100644 --- a/internal/domain/system.go +++ b/internal/domain/system.go @@ -172,8 +172,8 @@ func OfficialSystemUser() User { ID: OfficialSystemUserID, AccessHash: 6599886787491911851, Phone: "42777", - FirstName: branding.ProductName, - Username: branding.ProductUsername, + FirstName: branding.ProductName(), + Username: branding.ProductUsername(), Verified: true, Support: true, } diff --git a/internal/domain/welcome_message.go b/internal/domain/welcome_message.go index a398d778..a60df2cf 100644 --- a/internal/domain/welcome_message.go +++ b/internal/domain/welcome_message.go @@ -6,7 +6,12 @@ import ( "strings" ) -const officialWelcomeMessageTemplate = "👋 Welcome to OwpenGram!\n\nYou just signed in via %s.\n\nIf this wasn't you, revoke this session from \"Settings > Privacy and Security > Active sessions\" immediately." +// officialUpdatesChannelMention is the public @username of the updates channel +// linked from the welcome message. It is carried both in the template text and +// in a MessageEntityMention so clients render it as a tappable link. +const officialUpdatesChannelMention = "@ziodotsh" + +const officialWelcomeMessageTemplate = "👋 Welcome to OwpenGram!\n\nYou just signed in via %s.\n\nIf this wasn't you, revoke this session from \"Settings > Privacy and Security > Active sessions\" immediately.\n\nIf you haven't already, feel free to join " + officialUpdatesChannelMention + " for all the latest updates!" // OfficialWelcomeMessage builds the account-visible incoming message sent // from the official system account on every completed sign-in (SignUp and @@ -20,13 +25,43 @@ func OfficialWelcomeMessage(userID int64, method string, date int) (Message, err if userID <= 0 || IsSystemUserID(userID) || method == "" || date < 0 || date > math.MaxInt32 { return Message{}, fmt.Errorf("%w: user=%d method=%q date=%d", ErrLoginCodeDeliveryInvalid, userID, method, date) } - return Message{ + body := fmt.Sprintf(officialWelcomeMessageTemplate, method) + msg := Message{ OwnerUserID: userID, Peer: Peer{Type: PeerTypeUser, ID: OfficialSystemUserID}, From: Peer{Type: PeerTypeUser, ID: OfficialSystemUserID}, Date: date, - Body: fmt.Sprintf(officialWelcomeMessageTemplate, method), - }, nil + Body: body, + } + if ent, ok := usernameMentionEntity(body, officialUpdatesChannelMention); ok { + msg.Entities = []MessageEntity{ent} + } + return msg, nil +} + +// usernameMentionEntity locates mention ("@name") in text and returns a +// MessageEntityMention spanning it, with the UTF-16 offset/length clients expect. +func usernameMentionEntity(text, mention string) (MessageEntity, bool) { + before, _, found := strings.Cut(text, mention) + if !found { + return MessageEntity{}, false + } + return MessageEntity{ + Type: MessageEntityMention, + Offset: utf16Len(before), + Length: utf16Len(mention), + }, true +} + +func utf16Len(s string) int { + n := 0 + for _, r := range s { + n++ + if r > 0xffff { + n++ + } + } + return n } // SignInMethodLabel returns the human-readable method name embedded in diff --git a/internal/domain/welcome_message_test.go b/internal/domain/welcome_message_test.go new file mode 100644 index 00000000..ea5c0d87 --- /dev/null +++ b/internal/domain/welcome_message_test.go @@ -0,0 +1,29 @@ +package domain + +import ( + "testing" + "unicode/utf16" +) + +func TestOfficialWelcomeMessageLinksUpdatesChannel(t *testing.T) { + for _, method := range []string{"phone number", "email"} { + msg, err := OfficialWelcomeMessage(1780243200, method, 1_700_000_000) + if err != nil { + t.Fatalf("method %q: %v", method, err) + } + if len(msg.Entities) != 1 { + t.Fatalf("method %q: entities = %+v, want one mention", method, msg.Entities) + } + ent := msg.Entities[0] + if ent.Type != MessageEntityMention { + t.Fatalf("entity type = %q, want mention", ent.Type) + } + units := utf16.Encode([]rune(msg.Body)) + if ent.Offset < 0 || ent.Length <= 0 || ent.Offset+ent.Length > len(units) { + t.Fatalf("entity %+v out of bounds for body of %d utf16 units", ent, len(units)) + } + if got := string(utf16.Decode(units[ent.Offset : ent.Offset+ent.Length])); got != officialUpdatesChannelMention { + t.Fatalf("entity spans %q, want %q", got, officialUpdatesChannelMention) + } + } +} diff --git a/internal/mtprotoedge/bot_callback_e2e_test.go b/internal/mtprotoedge/bot_callback_e2e_test.go index dcd34342..5b7e00b3 100644 --- a/internal/mtprotoedge/bot_callback_e2e_test.go +++ b/internal/mtprotoedge/bot_callback_e2e_test.go @@ -230,9 +230,9 @@ func TestBotInlineKeyboardCallbackFlow(t *testing.T) { return fmt.Errorf("bot getUsers(owner) = %d, want 1", len(got)) } ownerSeen := got[0].(*tg.User) - markup := &tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButtonClass{ - &tg.KeyboardButtonCallback{Text: "Press", Data: callbackData}, - &tg.KeyboardButtonURL{Text: "Site", URL: "https://example.com/x"}, + markup := &tg.ReplyInlineMarkup{Rows: []tg.KeyboardInlineButtonRow{{Buttons: []tg.KeyboardInlineButton{ + {Text: "Press", Type: &tg.InlineButtonTypeCallback{Data: callbackData}}, + {Text: "Site", Type: &tg.InlineButtonTypeURL{URL: "https://example.com/x"}}, }}}} req := &tg.MessagesSendMessageRequest{ Peer: &tg.InputPeerUser{UserID: ownerSeen.ID, AccessHash: ownerSeen.AccessHash}, @@ -312,15 +312,15 @@ func TestBotInlineKeyboardCallbackFlow(t *testing.T) { if !ok || len(inline.Rows) != 1 || len(inline.Rows[0].Buttons) != 2 { t.Fatalf("unexpected markup shape: %#v", rm) } - cbBtn, ok := inline.Rows[0].Buttons[0].(*tg.KeyboardButtonCallback) + cbBtn, ok := inline.Rows[0].Buttons[0].Type.(*tg.InlineButtonTypeCallback) if !ok { - t.Fatalf("first button not callback: %#v", inline.Rows[0].Buttons[0]) + t.Fatalf("first button not callback: %#v", inline.Rows[0].Buttons[0].Type) } if string(cbBtn.Data) != string(callbackData) { t.Fatalf("callback data round-trip mismatch: got %v want %v", cbBtn.Data, callbackData) } - if _, ok := inline.Rows[0].Buttons[1].(*tg.KeyboardButtonURL); !ok { - t.Fatalf("second button not url: %#v", inline.Rows[0].Buttons[1]) + if _, ok := inline.Rows[0].Buttons[1].Type.(*tg.InlineButtonTypeURL); !ok { + t.Fatalf("second button not url: %#v", inline.Rows[0].Buttons[1].Type) } msgID = msg.ID } diff --git a/internal/otpdelivery/smtp/sender.go b/internal/otpdelivery/smtp/sender.go index d3e07545..90267cd3 100644 --- a/internal/otpdelivery/smtp/sender.go +++ b/internal/otpdelivery/smtp/sender.go @@ -3,7 +3,9 @@ package smtp import ( "bytes" "context" + "crypto/rand" "crypto/tls" + "encoding/hex" "fmt" "mime" "net" @@ -146,6 +148,8 @@ func buildMessage(from, to, subject, body string) []byte { var b bytes.Buffer b.WriteString("From: " + from + "\r\n") b.WriteString("To: " + to + "\r\n") + b.WriteString("Date: " + time.Now().Format(time.RFC1123Z) + "\r\n") + b.WriteString("Message-ID: " + generateMessageID(from) + "\r\n") b.WriteString("Subject: " + mime.QEncoding.Encode("utf-8", subject) + "\r\n") b.WriteString("MIME-Version: 1.0\r\n") b.WriteString("Content-Type: text/plain; charset=utf-8\r\n") @@ -155,6 +159,25 @@ func buildMessage(from, to, subject, body string) []byte { return b.Bytes() } +// generateMessageID builds a Message-ID header value (RFC 5322 3.6.4), using +// the sending domain parsed out of the From address and a random token so +// each message gets a unique id even under concurrent sends. +func generateMessageID(from string) string { + domain := "localhost" + if addr, err := stdmail.ParseAddress(from); err == nil { + if i := strings.LastIndex(addr.Address, "@"); i >= 0 { + domain = addr.Address[i+1:] + } + } + var raw [16]byte + if _, err := rand.Read(raw[:]); err != nil { + // crypto/rand failing is effectively unheard of, but fall back to a + // time-based token rather than emit a non-unique Message-ID. + return fmt.Sprintf("<%d@%s>", time.Now().UnixNano(), domain) + } + return fmt.Sprintf("<%s@%s>", hex.EncodeToString(raw[:]), domain) +} + // emailContent builds the login-code email subject/body, branded with the // operator's configured product name (Config.AppName) instead of the // package's internal "telesrv" fallback. diff --git a/internal/rpc/aicompose_webpage.go b/internal/rpc/aicompose_webpage.go index 081fea46..148b0824 100644 --- a/internal/rpc/aicompose_webpage.go +++ b/internal/rpc/aicompose_webpage.go @@ -43,7 +43,7 @@ func (r *Router) resolveAIComposeStyleWebPage(ctx context.Context, rawURL string Hash: aiComposeToneWebPageHash(tone), Date: int(now.Unix()), Type: aiComposeToneWebPageType, - SiteName: branding.ProductName, + SiteName: branding.ProductName(), Title: tone.Title, Description: tone.Prompt, ComposeToneEmojiID: tone.EmojiID, diff --git a/internal/rpc/botapi_gateway.go b/internal/rpc/botapi_gateway.go index c6a02a56..8bca5446 100644 --- a/internal/rpc/botapi_gateway.go +++ b/internal/rpc/botapi_gateway.go @@ -33,6 +33,148 @@ func (r *Router) BotAPISelf(ctx context.Context, botID int64) (domain.User, erro return u, nil } +// BotAPIChat resolves a chat for the Bot API getChat method. chat_id is numeric +// only (no @username). Public channels/supergroups resolve even when the bot is +// not a member; a private chat the bot cannot access is CHAT_NOT_FOUND. +func (r *Router) BotAPIChat(ctx context.Context, botID, chatID int64) (domain.BotAPIChat, error) { + if r == nil || botID == 0 { + return domain.BotAPIChat{}, errors.New("BOT_INVALID") + } + peer, ok := botAPIPeerFromChatID(chatID) + if !ok { + return domain.BotAPIChat{}, errors.New("CHAT_ID_INVALID") + } + switch peer.Type { + case domain.PeerTypeUser: + if r.deps.Users == nil { + return domain.BotAPIChat{}, errors.New("CHAT_NOT_FOUND") + } + u, found, err := r.deps.Users.ByID(ctx, botID, peer.ID) + if err != nil { + return domain.BotAPIChat{}, err + } + if !found { + return domain.BotAPIChat{}, errors.New("CHAT_NOT_FOUND") + } + return domain.BotAPIChat{ + Peer: peer, + Type: "private", + FirstName: u.FirstName, + LastName: u.LastName, + Username: u.Username, + Verified: u.Verified, + Scam: u.Scam, + Fake: u.Fake, + }, nil + case domain.PeerTypeChannel: + if r.deps.Channels == nil { + return domain.BotAPIChat{}, errors.New("CHAT_NOT_FOUND") + } + view, err := r.deps.Channels.GetChannel(ctx, botID, peer.ID) + if err != nil { + return domain.BotAPIChat{}, botAPIChatErr(err) + } + ch := view.Channel + typ := "supergroup" + if ch.Broadcast && !ch.Megagroup { + typ = "channel" + } + out := domain.BotAPIChat{ + Peer: peer, + Type: typ, + Title: ch.Title, + Username: ch.Username, + Description: ch.About, + IsForum: ch.Forum, + Verified: ch.Verified, + Scam: ch.Scam, + Fake: ch.Fake, + SlowModeDelay: ch.SlowmodeSeconds, + LinkedChatID: ch.LinkedChatID, + } + if typ == "supergroup" { + perms := ch.DefaultBannedRights + out.Permissions = &perms + } + if ch.PinnedMessageID > 0 { + if hist, msgErr := r.deps.Channels.GetMessages(ctx, botID, peer.ID, []int{ch.PinnedMessageID}); msgErr == nil && len(hist.Messages) > 0 { + pinned := botAPIMessageFromChannel(botID, hist.Messages[0]) + out.PinnedMessage = &pinned + out.PinnedMessageUsers = hist.Users + } + } + return out, nil + } + return domain.BotAPIChat{}, errors.New("CHAT_ID_INVALID") +} + +// BotAPIChatMemberCount resolves getChatMemberCount. Channels/supergroups only. +func (r *Router) BotAPIChatMemberCount(ctx context.Context, botID, chatID int64) (int, error) { + if r == nil || botID == 0 { + return 0, errors.New("BOT_INVALID") + } + peer, ok := botAPIPeerFromChatID(chatID) + if !ok || peer.Type != domain.PeerTypeChannel { + return 0, errors.New("CHAT_ID_INVALID") + } + if r.deps.Channels == nil { + return 0, errors.New("CHAT_NOT_FOUND") + } + view, err := r.deps.Channels.ResolveChannel(ctx, botID, peer.ID) + if err != nil { + return 0, botAPIChatErr(err) + } + return view.Channel.ParticipantsCount, nil +} + +// BotAPIChatMember resolves getChatMember. Channels/supergroups only. +func (r *Router) BotAPIChatMember(ctx context.Context, botID, chatID, userID int64) (domain.BotAPIChatMember, error) { + if r == nil || botID == 0 { + return domain.BotAPIChatMember{}, errors.New("BOT_INVALID") + } + if userID <= 0 { + return domain.BotAPIChatMember{}, errors.New("USER_ID_INVALID") + } + peer, ok := botAPIPeerFromChatID(chatID) + if !ok || peer.Type != domain.PeerTypeChannel { + return domain.BotAPIChatMember{}, errors.New("CHAT_ID_INVALID") + } + if r.deps.Channels == nil { + return domain.BotAPIChatMember{}, errors.New("CHAT_NOT_FOUND") + } + member, err := r.deps.Channels.GetParticipant(ctx, botID, peer.ID, userID) + switch { + case err == nil: + case errors.Is(err, domain.ErrUserNotParticipant): + // Bot API returns a "left" member for a user who is simply not in the + // chat, as long as the chat itself is accessible. + member = domain.ChannelMember{ChannelID: peer.ID, UserID: userID, Role: domain.ChannelRoleMember, Status: domain.ChannelMemberLeft} + default: + return domain.BotAPIChatMember{}, botAPIChatErr(err) + } + out := domain.BotAPIChatMember{Member: member} + if r.deps.Users != nil { + if u, found, uErr := r.deps.Users.ByID(ctx, botID, userID); uErr == nil && found { + out.User = u + } + } + if out.User.ID == 0 { + out.User = domain.User{ID: userID} + } + return out, nil +} + +func botAPIChatErr(err error) error { + switch { + case errors.Is(err, domain.ErrChannelInvalid), + errors.Is(err, domain.ErrChannelPrivate), + errors.Is(err, domain.ErrChannelUserBanned): + return errors.New("CHAT_NOT_FOUND") + default: + return channelInvalidErr(err) + } +} + // BotAPIUpdates returns durable update_id based events projected for the HTTP // Bot API. New deployments use the dedicated Bot API queue; the legacy // user_update_events fallback is kept for tests that have not wired the queue. diff --git a/internal/rpc/bots_inline_rpc_test.go b/internal/rpc/bots_inline_rpc_test.go index 42ad7798..7e135c7d 100644 --- a/internal/rpc/bots_inline_rpc_test.go +++ b/internal/rpc/bots_inline_rpc_test.go @@ -844,8 +844,8 @@ func TestInlineBotArticleTextChannelRoundTrip(t *testing.T) { } editReq := &tg.MessagesEditInlineBotMessageRequest{ID: msgID} editReq.SetMessage("inline group edited") - editReq.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{ - Buttons: []tg.KeyboardButtonClass{&tg.KeyboardButtonCallback{Text: "Done", Data: []byte("v2")}}, + editReq.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardInlineButtonRow{{ + Buttons: []tg.KeyboardInlineButton{{Text: "Done", Type: &tg.InlineButtonTypeCallback{Data: []byte("v2")}}}, }}}) if ok, err := f.router.onMessagesEditInlineBotMessage(botCtx, editReq); err != nil || !ok { t.Fatalf("channel inline edit = %v,%v, want true,nil", ok, err) @@ -2065,12 +2065,13 @@ func assertTGInlineReplyMarkup(t *testing.T, msg *tg.Message, wantText string, w if len(markup.Rows) != 1 || len(markup.Rows[0].Buttons) != 1 { t.Fatalf("reply_markup rows = %+v, want one callback button", markup.Rows) } - button, ok := markup.Rows[0].Buttons[0].(*tg.KeyboardButtonCallback) + button := markup.Rows[0].Buttons[0] + buttonType, ok := button.Type.(*tg.InlineButtonTypeCallback) if !ok { - t.Fatalf("reply_markup button = %T, want callback", markup.Rows[0].Buttons[0]) + t.Fatalf("reply_markup button = %T, want callback", button.Type) } - if button.Text != wantText || !bytes.Equal(button.Data, wantData) { - t.Fatalf("reply_markup button = %q/%v, want %q/%v", button.Text, button.Data, wantText, wantData) + if button.Text != wantText || !bytes.Equal(buttonType.Data, wantData) { + t.Fatalf("reply_markup button = %q/%v, want %q/%v", button.Text, buttonType.Data, wantText, wantData) } } @@ -2119,8 +2120,8 @@ func inlineArticleResult(id, message string) tg.InputBotInlineResultClass { func inlineArticleResultWithCallback(id, message, button string, data []byte) tg.InputBotInlineResultClass { result := inlineArticleResult(id, message).(*tg.InputBotInlineResult) msg := result.SendMessage.(*tg.InputBotInlineMessageText) - msg.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{ - Buttons: []tg.KeyboardButtonClass{&tg.KeyboardButtonCallback{Text: button, Data: data}}, + msg.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardInlineButtonRow{{ + Buttons: []tg.KeyboardInlineButton{{Text: button, Type: &tg.InlineButtonTypeCallback{Data: data}}}, }}}) return result } @@ -2308,8 +2309,8 @@ func inlineContactResult(id, phone, first, last, vcard string) *tg.InputBotInlin func inlineContactResultWithCallback(id, phone, first, last string, data []byte) tg.InputBotInlineResultClass { result := inlineContactResult(id, phone, first, last, "BEGIN:VCARD\nFN:"+first+" "+last+"\nEND:VCARD") msg := result.SendMessage.(*tg.InputBotInlineMessageMediaContact) - msg.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{ - Buttons: []tg.KeyboardButtonClass{&tg.KeyboardButtonCallback{Text: "Contact", Data: data}}, + msg.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardInlineButtonRow{{ + Buttons: []tg.KeyboardInlineButton{{Text: "Contact", Type: &tg.InlineButtonTypeCallback{Data: data}}}, }}}) return result } diff --git a/internal/rpc/bots_longtail.go b/internal/rpc/bots_longtail.go index ae18d8ba..982fc539 100644 --- a/internal/rpc/bots_longtail.go +++ b/internal/rpc/bots_longtail.go @@ -594,7 +594,7 @@ func (r *Router) onBotsRequestWebViewButton(ctx context.Context, req *tg.BotsReq if err != nil { return nil, internalErr() } - if req.UserID == nil || req.Button == nil { + if req.UserID == nil || req.Button.Type == nil { return nil, buttonDataInvalidErr() } if r.deps.Bots == nil { @@ -622,21 +622,21 @@ func (r *Router) onBotsRequestWebViewButton(ctx context.Context, req *tg.BotsReq return &tg.BotsRequestedButton{WebappReqID: saved.WebAppReqID}, nil } -func (r *Router) onBotsGetRequestedWebViewButton(ctx context.Context, req *tg.BotsGetRequestedWebViewButtonRequest) (tg.KeyboardButtonClass, error) { +func (r *Router) onBotsGetRequestedWebViewButton(ctx context.Context, req *tg.BotsGetRequestedWebViewButtonRequest) (tg.KeyboardButton, error) { userID, _, err := r.currentUserID(ctx) if err != nil { - return nil, internalErr() + return tg.KeyboardButton{}, internalErr() } bot, err := r.resolveBotUserForViewer(ctx, userID, req.Bot) if err != nil { - return nil, err + return tg.KeyboardButton{}, err } button, found, err := r.deps.Bots.GetRequestedWebViewButton(ctx, bot.ID, userID, req.WebappReqID) if err != nil { - return nil, internalErr() + return tg.KeyboardButton{}, internalErr() } if !found { - return nil, buttonDataInvalidErr() + return tg.KeyboardButton{}, buttonDataInvalidErr() } return tgKeyboardButtonRequestPeer(button), nil } @@ -761,21 +761,20 @@ func (r *Router) tgBotPreviewMedia(ctx context.Context, item domain.BotAppPrevie return out } -func domainRequestedButtonFromTG(botUserID int64, _ tg.InputUserClass, button tg.KeyboardButtonClass) (domain.BotRequestedWebViewButton, error) { +func domainRequestedButtonFromTG(botUserID int64, _ tg.InputUserClass, button tg.KeyboardButton) (domain.BotRequestedWebViewButton, error) { var out domain.BotRequestedWebViewButton out.BotUserID = botUserID - switch b := button.(type) { - case *tg.InputKeyboardButtonRequestPeer: + out.Text = strings.TrimSpace(button.Text) + switch b := button.Type.(type) { + case *tg.InputButtonTypeRequestPeer: out.ButtonID = b.ButtonID - out.Text = strings.TrimSpace(b.Text) out.PeerType, out.PeerFilter = domainRequestPeerFilter(b.PeerType) out.MaxQuantity = b.MaxQuantity out.NameRequested = b.NameRequested out.UsernameRequested = b.UsernameRequested out.PhotoRequested = b.PhotoRequested - case *tg.KeyboardButtonRequestPeer: + case *tg.ButtonTypeRequestPeer: out.ButtonID = b.ButtonID - out.Text = strings.TrimSpace(b.Text) out.PeerType, out.PeerFilter = domainRequestPeerFilter(b.PeerType) out.MaxQuantity = b.MaxQuantity default: @@ -800,12 +799,14 @@ func requestPeerTypeName(peerType tg.RequestPeerTypeClass) string { } } -func tgKeyboardButtonRequestPeer(button domain.BotRequestedWebViewButton) tg.KeyboardButtonClass { - return &tg.KeyboardButtonRequestPeer{ - Text: button.Text, - ButtonID: button.ButtonID, - PeerType: tgRequestPeerTypeWithFilter(button.PeerType, button.PeerFilter), - MaxQuantity: button.MaxQuantity, +func tgKeyboardButtonRequestPeer(button domain.BotRequestedWebViewButton) tg.KeyboardButton { + return tg.KeyboardButton{ + Text: button.Text, + Type: &tg.ButtonTypeRequestPeer{ + ButtonID: button.ButtonID, + PeerType: tgRequestPeerTypeWithFilter(button.PeerType, button.PeerFilter), + MaxQuantity: button.MaxQuantity, + }, } } diff --git a/internal/rpc/bots_longtail_rpc_test.go b/internal/rpc/bots_longtail_rpc_test.go index ac647f30..55e07644 100644 --- a/internal/rpc/bots_longtail_rpc_test.go +++ b/internal/rpc/bots_longtail_rpc_test.go @@ -252,9 +252,9 @@ func TestBotsLongtailCommercialAndSettingsStubs(t *testing.T) { } if _, err := f.router.onBotsRequestWebViewButton(botCtx, &tg.BotsRequestWebViewButtonRequest{ UserID: inputUser(f.owner), - Button: &tg.KeyboardButtonSimpleWebView{ + Button: tg.KeyboardButton{ Text: "Open", - URL: "https://example.com/app", + Type: &tg.ButtonTypeSimpleWebView{URL: "https://example.com/app"}, }, }); !tgerr.Is(err, "BUTTON_DATA_INVALID") { t.Fatalf("request webview button err = %v, want BUTTON_DATA_INVALID", err) diff --git a/internal/rpc/channels_leave_rpc_test.go b/internal/rpc/channels_leave_rpc_test.go index 31e53c8a..a2f3acee 100644 --- a/internal/rpc/channels_leave_rpc_test.go +++ b/internal/rpc/channels_leave_rpc_test.go @@ -9,6 +9,7 @@ import ( "github.com/iamxvbaba/td/tgerr" "go.uber.org/zap/zaptest" + appaccount "telesrv/internal/app/account" appchannels "telesrv/internal/app/channels" appusers "telesrv/internal/app/users" "telesrv/internal/domain" @@ -26,6 +27,10 @@ func (acceptPasswordAccountService) CheckPassword(_ context.Context, _ int64, ch return nil } +func (acceptPasswordAccountService) GetPassword(_ context.Context, _ int64) (domain.PasswordSettings, error) { + return domain.PasswordSettings{HasPassword: true}, nil +} + func TestMessagesGetFutureChatCreatorAfterLeaveAndCreatorLeaveTransfers(t *testing.T) { ctx := context.Background() userStore := memory.NewUserStore() @@ -109,6 +114,60 @@ func TestMessagesGetFutureChatCreatorAfterLeaveAndCreatorLeaveTransfers(t *testi } } +func TestLeaveChannelInvalidatesStaleFullChannelProjection(t *testing.T) { + ctx := context.Background() + userStore := memory.NewUserStore() + owner, _ := userStore.Create(ctx, domain.User{AccessHash: 9401, Phone: "15550009401", FirstName: "Owner"}) + member, _ := userStore.Create(ctx, domain.User{AccessHash: 9402, Phone: "15550009402", FirstName: "Member"}) + channelStore := memory.NewChannelStore() + channelService := appchannels.NewService(channelStore) + r := New(Config{}, Deps{ + Users: appusers.NewService(userStore), + Channels: channelService, + }, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700009400, 0)}) + created, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{ + CreatorUserID: owner.ID, + Title: "leave projection", + Megagroup: true, + Date: 1700009400, + }) + if err != nil { + t.Fatalf("create channel: %v", err) + } + if _, err := channelService.UpdateUsername(ctx, owner.ID, domain.UpdateChannelUsernameRequest{ + ChannelID: created.Channel.ID, + Username: "leave_projection_pub", + }); err != nil { + t.Fatalf("publish channel: %v", err) + } + inputChannel := &tg.InputChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash} + + if _, err := r.onChannelsJoinChannel(WithUserID(ctx, member.ID), inputChannel); err != nil { + t.Fatalf("member joins: %v", err) + } + // Warm the channels.getFullChannel projection cache while still a member. + full, err := r.onChannelsGetFullChannel(WithUserID(ctx, member.ID), inputChannel) + if err != nil { + t.Fatalf("full channel while joined: %v", err) + } + if chat, ok := full.Chats[0].(*tg.Channel); !ok || chat.Left { + t.Fatalf("joined full chat = %#v, want member (not left)", full.Chats[0]) + } + + if _, err := r.onChannelsLeaveChannel(WithUserID(ctx, member.ID), inputChannel); err != nil { + t.Fatalf("member leaves: %v", err) + } + + after, err := r.onChannelsGetFullChannel(WithUserID(ctx, member.ID), inputChannel) + if err != nil { + t.Fatalf("full channel after leave: %v", err) + } + chat, ok := after.Chats[0].(*tg.Channel) + if !ok || !chat.Left { + t.Fatalf("post-leave full chat = %#v, want left=true (stale projection served)", after.Chats[0]) + } +} + func TestMessagesEditChatCreatorTransfersWithoutChannelPts(t *testing.T) { ctx := context.Background() userStore := memory.NewUserStore() @@ -188,6 +247,58 @@ func TestMessagesEditChatCreatorTransfersWithoutChannelPts(t *testing.T) { } } +// TestMessagesEditChatCreatorRequiresPasswordSetup covers an owner who has never +// enabled two-step verification: the client's transfer-ownership probe +// (inputUserEmpty + inputCheckPasswordEmpty) must get PASSWORD_MISSING, not +// PASSWORD_HASH_INVALID -- the desktop client only recognizes PASSWORD_MISSING +// to show its "enable 2FA first" box, and otherwise falls through into a real +// password-entry flow it can't satisfy. +func TestMessagesEditChatCreatorRequiresPasswordSetup(t *testing.T) { + ctx := context.Background() + userStore := memory.NewUserStore() + owner, err := userStore.Create(ctx, domain.User{AccessHash: 9221, Phone: "15550009221", FirstName: "Owner"}) + if err != nil { + t.Fatalf("create owner: %v", err) + } + member, err := userStore.Create(ctx, domain.User{AccessHash: 9222, Phone: "15550009222", FirstName: "Member"}) + if err != nil { + t.Fatalf("create member: %v", err) + } + channelStore := memory.NewChannelStore() + channelService := appchannels.NewService(channelStore) + r := New(Config{}, Deps{ + Account: appaccount.NewService(memory.NewPasswordStore()), + Users: appusers.NewService(userStore), + Channels: channelService, + }, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700009130, 0)}) + created, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{ + CreatorUserID: owner.ID, + Title: "no password owner", + Megagroup: true, + MemberUserIDs: []int64{member.ID}, + Date: 1700009130, + }) + if err != nil { + t.Fatalf("create channel: %v", err) + } + ownerCtx := WithUserID(ctx, owner.ID) + peer := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash} + if _, err := r.onMessagesEditChatCreator(ownerCtx, &tg.MessagesEditChatCreatorRequest{ + Peer: peer, + UserID: &tg.InputUserEmpty{}, + Password: &tg.InputCheckPasswordEmpty{}, + }); err == nil || !tgerr.Is(err, "PASSWORD_MISSING") { + t.Fatalf("editChatCreator probe err = %v, want PASSWORD_MISSING", err) + } + if _, err := r.onMessagesEditChatCreator(ownerCtx, &tg.MessagesEditChatCreatorRequest{ + Peer: peer, + UserID: &tg.InputUser{UserID: member.ID, AccessHash: member.AccessHash}, + Password: &tg.InputCheckPasswordSRP{SRPID: 1, A: []byte{1}, M1: []byte{2}}, + }); err == nil || !tgerr.Is(err, "PASSWORD_MISSING") { + t.Fatalf("editChatCreator transfer err = %v, want PASSWORD_MISSING", err) + } +} + func TestMessagesGetFutureChatCreatorAfterLeaveNoCandidate(t *testing.T) { ctx := context.Background() userStore := memory.NewUserStore() diff --git a/internal/rpc/channels_legacy_chat.go b/internal/rpc/channels_legacy_chat.go index b3d653ff..999a3dbc 100644 --- a/internal/rpc/channels_legacy_chat.go +++ b/internal/rpc/channels_legacy_chat.go @@ -339,6 +339,20 @@ func (r *Router) onMessagesEditChatCreator(ctx context.Context, req *tg.Messages if req.Password == nil { return nil, passwordHashInvalidErr() } + if r.deps.Account == nil { + return nil, passwordHashInvalidErr() + } + // 转让所有权无条件要求已开启两步验证:先探测账号是否设有密码, + // 让 messages.editChatCreator 的探测请求(inputUserEmpty + inputCheckPasswordEmpty) + // 拿到 PASSWORD_MISSING 而非 PASSWORD_HASH_INVALID —— 客户端只识别前者来展示 + // “请先开启两步验证”提示,否则会误入真实密码校验流程并在没有密码可核对时崩溃。 + passwordSettings, err := r.deps.Account.GetPassword(ctx, userID) + if err != nil { + return nil, internalErr() + } + if !passwordSettings.HasPassword { + return nil, passwordMissingErr() + } if _, ok := req.UserID.(*tg.InputUserEmpty); ok { return nil, passwordHashInvalidErr() } @@ -355,9 +369,6 @@ func (r *Router) onMessagesEditChatCreator(ctx context.Context, req *tg.Messages if target.Bot { return nil, userIDInvalidErr() } - if r.deps.Account == nil { - return nil, passwordHashInvalidErr() - } if err := r.deps.Account.CheckPassword(ctx, userID, domainPasswordCheck(req.Password)); err != nil { return nil, passwordErr(err) } diff --git a/internal/rpc/channels_members.go b/internal/rpc/channels_members.go index 0ad8b911..5a3fc768 100644 --- a/internal/rpc/channels_members.go +++ b/internal/rpc/channels_members.go @@ -341,6 +341,7 @@ func (r *Router) onChannelsInviteToChannel(ctx context.Context, req *tg.Channels return nil, channelInviteErr(err) } r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) + r.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members)) r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...) cache := newViewerPeerCache(r) updates := r.channelOperationUpdatesWithPeerCache(ctx, userID, res, cache) @@ -379,6 +380,7 @@ func (r *Router) onChannelsJoinChannel(ctx context.Context, input tg.InputChanne return nil, channelInviteErr(err) } r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) + r.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members)) r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...) updates := r.channelOperationUpdates(ctx, userID, res) r.pushChannelUpdates(ctx, userID, res.Channel.ID, res.Recipients, func(viewerUserID int64) *tg.Updates { @@ -406,6 +408,11 @@ func (r *Router) onChannelsLeaveChannel(ctx context.Context, input tg.InputChann return nil, channelAdminErr(err) } r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) + membershipChanged := channelMemberUserIDs(res.Members) + if len(membershipChanged) == 0 { + membershipChanged = []int64{userID} + } + r.invalidateChannelMembershipProjection(res.Channel.ID, membershipChanged) r.removeOnlineChannelMemberships(res.Channel.ID, userID) r.recordChannelStateForUser(ctx, userID, res.Channel.ID, true) updates := r.channelOperationUpdates(ctx, userID, res) @@ -658,6 +665,7 @@ func (r *Router) onMessagesHideChatJoinRequest(ctx context.Context, req *tg.Mess return nil, channelInviteErr(err) } r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) + r.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members)) r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...) updates := r.channelOperationUpdates(ctx, userID, res) r.appendPendingJoinRequestsUpdate(ctx, userID, updates, res.Channel) @@ -696,6 +704,7 @@ func (r *Router) onMessagesHideAllChatJoinRequests(ctx context.Context, req *tg. return nil, channelInviteErr(err) } r.invalidateChannelFullBotInfoCacheForChannel(res.Channel.ID) + r.invalidateChannelMembershipProjection(res.Channel.ID, channelMemberUserIDs(res.Members)) r.addOnlineChannelMemberships(res.Channel.ID, channelMemberUserIDs(res.Members)...) updates := r.channelOperationUpdates(ctx, userID, res) r.appendPendingJoinRequestsUpdate(ctx, userID, updates, res.Channel) diff --git a/internal/rpc/channels_updates_rpc_test.go b/internal/rpc/channels_updates_rpc_test.go index b05d0b08..e663f59c 100644 --- a/internal/rpc/channels_updates_rpc_test.go +++ b/internal/rpc/channels_updates_rpc_test.go @@ -2,10 +2,6 @@ package rpc import ( "context" - "github.com/iamxvbaba/td/clock" - "github.com/iamxvbaba/td/proto" - "github.com/iamxvbaba/td/tg" - "go.uber.org/zap/zaptest" "strings" appchannels "telesrv/internal/app/channels" appupdates "telesrv/internal/app/updates" @@ -13,6 +9,11 @@ import ( "telesrv/internal/domain" "telesrv/internal/store/memory" "testing" + + "github.com/iamxvbaba/td/clock" + "github.com/iamxvbaba/td/proto" + "github.com/iamxvbaba/td/tg" + "go.uber.org/zap/zaptest" ) func TestChannelRealtimeRecipientsPreferOnlineMembers(t *testing.T) { @@ -640,6 +641,39 @@ func TestChannelUnreadMentionsRPCUsesMentionState(t *testing.T) { } } +// A channel post containing an @token that is not a syntactically valid +// username (too short, leading digit, etc.) must still send. Real Telegram +// renders it as a mention and only fails when the reader taps it; it never +// rejects the send. Regression for a 500 INTERNAL_SERVER_ERROR. +func TestChannelSendMessageWithUnresolvableMentionSucceeds(t *testing.T) { + ctx := context.Background() + userStore := memory.NewUserStore() + owner, _ := userStore.Create(ctx, domain.User{AccessHash: 9201, Phone: "15550009201", FirstName: "Owner", Username: "owner_badmention"}) + channelStore := memory.NewChannelStore() + channelService := appchannels.NewService(channelStore) + r := New(Config{}, Deps{ + Users: appusers.NewService(userStore), + Channels: channelService, + }, zaptest.NewLogger(t), clock.System) + created, err := channelService.CreateMegagroupFromCreateChat(ctx, owner.ID, domain.CreateChannelRequest{ + Title: "Bad Mention", + Date: 1700009201, + }) + if err != nil { + t.Fatalf("create megagroup: %v", err) + } + peer := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash} + for i, text := range []string{"look at @ziodotsh", "hi @2cool and @_x"} { + if _, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{ + Peer: peer, + Message: text, + RandomID: int64(9202001 + i), + }); err != nil { + t.Fatalf("send %q: unexpected error %v", text, err) + } + } +} + func TestChannelDifferenceIncludesExtraForwardSourceChannel(t *testing.T) { channel := domain.Channel{ID: 2000000100, AccessHash: 9010, Title: "Megagroup", Megagroup: true, Date: 1700000000, Pts: 3} source := domain.Channel{ID: 2000000101, AccessHash: 9011, Title: "Source", Broadcast: true, Date: 1700000000} diff --git a/internal/rpc/convert_markup.go b/internal/rpc/convert_markup.go index 9005e162..5d1e8f90 100644 --- a/internal/rpc/convert_markup.go +++ b/internal/rpc/convert_markup.go @@ -229,34 +229,38 @@ func domainOutgoingReplyMarkupForSender(markup tg.ReplyMarkupClass, senderIsBot } } -func domainReplyKeyboardButton(button tg.KeyboardButtonClass) (domain.MarkupButton, error) { - style, icon, err := domainMarkupButtonStyle(button) +func domainReplyKeyboardButton(button tg.KeyboardButton) (domain.MarkupButton, error) { + style, icon, err := domainMarkupButtonStyle(button.GetStyle()) if err != nil { return domain.MarkupButton{}, err } - base := domain.MarkupButton{Style: style, IconCustomEmojiID: icon} - switch b := button.(type) { - case *tg.KeyboardButton: - base.Type, base.Text = domain.MarkupButtonText, b.Text - case *tg.KeyboardButtonRequestPhone: - base.Type, base.Text = domain.MarkupButtonRequestPhone, b.Text - case *tg.KeyboardButtonRequestGeoLocation: - base.Type, base.Text = domain.MarkupButtonRequestLocation, b.Text - case *tg.KeyboardButtonRequestPoll: - base.Type, base.Text = domain.MarkupButtonRequestPoll, b.Text - if quiz, ok := b.GetQuiz(); ok { + base := domain.MarkupButton{Style: style, IconCustomEmojiID: icon, Text: button.Text} + switch t := button.Type.(type) { + case nil, *tg.ButtonTypeDefault: + base.Type = domain.MarkupButtonText + case *tg.ButtonTypeRequestPhone: + base.Type = domain.MarkupButtonRequestPhone + case *tg.ButtonTypeRequestGeoLocation: + base.Type = domain.MarkupButtonRequestLocation + case *tg.ButtonTypeRequestPoll: + base.Type = domain.MarkupButtonRequestPoll + if quiz, ok := t.GetQuiz(); ok { if quiz { base.PollType = "quiz" } else { base.PollType = "regular" } } - case *tg.KeyboardButtonRequestPeer: - base.Type, base.Text = domain.MarkupButtonRequestPeer, b.Text - base.ButtonID, base.MaxQuantity = b.ButtonID, b.MaxQuantity - base.RequestPeerType, base.RequestPeerFilter = domainRequestPeerFilter(b.PeerType) - case *tg.KeyboardButtonSimpleWebView: - base.Type, base.Text, base.URL = domain.MarkupButtonSimpleWebView, b.Text, b.URL + case *tg.ButtonTypeRequestPeer: + base.Type = domain.MarkupButtonRequestPeer + base.ButtonID, base.MaxQuantity = t.ButtonID, t.MaxQuantity + base.RequestPeerType, base.RequestPeerFilter = domainRequestPeerFilter(t.PeerType) + case *tg.InputButtonTypeRequestPeer: + base.Type = domain.MarkupButtonRequestPeer + base.ButtonID, base.MaxQuantity = t.ButtonID, t.MaxQuantity + base.RequestPeerType, base.RequestPeerFilter = domainRequestPeerFilter(t.PeerType) + case *tg.ButtonTypeSimpleWebView: + base.Type, base.URL = domain.MarkupButtonSimpleWebView, t.URL default: return domain.MarkupButton{}, domain.ErrButtonTypeInvalid } @@ -281,29 +285,29 @@ func domainInlineMarkup(inline *tg.ReplyInlineMarkup) (*domain.MessageReplyMarku return out, nil } -func domainMarkupButton(btn tg.KeyboardButtonClass, buttonID int) (domain.MarkupButton, error) { - style, icon, err := domainMarkupButtonStyle(btn) +func domainMarkupButton(btn tg.KeyboardInlineButton, buttonID int) (domain.MarkupButton, error) { + style, icon, err := domainMarkupButtonStyle(btn.GetStyle()) if err != nil { return domain.MarkupButton{}, err } - switch b := btn.(type) { - case *tg.KeyboardButtonCallback: + switch t := btn.Type.(type) { + case *tg.InlineButtonTypeCallback: return domain.MarkupButton{ Type: domain.MarkupButtonCallback, - Text: b.Text, + Text: btn.Text, Style: style, IconCustomEmojiID: icon, - Data: append([]byte(nil), b.Data...), - RequiresPassword: b.RequiresPassword, + Data: append([]byte(nil), t.Data...), + RequiresPassword: t.RequiresPassword, }, nil - case *tg.KeyboardButtonURL: + case *tg.InlineButtonTypeURL: return domain.MarkupButton{ - Type: domain.MarkupButtonURL, Text: b.Text, URL: b.URL, + Type: domain.MarkupButtonURL, Text: btn.Text, URL: t.URL, Style: style, IconCustomEmojiID: icon, }, nil - case *tg.InputKeyboardButtonURLAuth: + case *tg.InputInlineButtonTypeURLAuth: botUserID := int64(0) - switch bot := b.Bot.(type) { + switch bot := t.Bot.(type) { case nil, *tg.InputUserEmpty, *tg.InputUserSelf: case *tg.InputUser: botUserID = bot.UserID @@ -311,34 +315,33 @@ func domainMarkupButton(btn tg.KeyboardButtonClass, buttonID int) (domain.Markup return domain.MarkupButton{}, domain.ErrButtonInvalid } return domain.MarkupButton{ - Type: domain.MarkupButtonLoginURL, Text: b.Text, URL: b.URL, - ForwardText: b.FwdText, ButtonID: buttonID, LoginBotUserID: botUserID, - RequestWriteAccess: b.RequestWriteAccess, Style: style, IconCustomEmojiID: icon, + Type: domain.MarkupButtonLoginURL, Text: btn.Text, URL: t.URL, + ForwardText: t.FwdText, ButtonID: buttonID, LoginBotUserID: botUserID, + RequestWriteAccess: t.RequestWriteAccess, Style: style, IconCustomEmojiID: icon, }, nil - case *tg.KeyboardButtonURLAuth: + case *tg.InlineButtonTypeURLAuth: return domain.MarkupButton{ - Type: domain.MarkupButtonLoginURL, Text: b.Text, URL: b.URL, - ForwardText: b.FwdText, ButtonID: b.ButtonID, + Type: domain.MarkupButtonLoginURL, Text: btn.Text, URL: t.URL, + ForwardText: t.FwdText, ButtonID: t.ButtonID, Style: style, IconCustomEmojiID: icon, }, nil - case *tg.KeyboardButtonWebView: - return domain.MarkupButton{Type: domain.MarkupButtonWebView, Text: b.Text, URL: b.URL, Style: style, IconCustomEmojiID: icon}, nil - case *tg.KeyboardButtonSwitchInline: - peerTypes, err := preparedInlinePeerTypesFromTG(b.PeerTypes) + case *tg.InlineButtonTypeWebView: + return domain.MarkupButton{Type: domain.MarkupButtonWebView, Text: btn.Text, URL: t.URL, Style: style, IconCustomEmojiID: icon}, nil + case *tg.InlineButtonTypeSwitchInline: + peerTypes, err := preparedInlinePeerTypesFromTG(t.PeerTypes) if err != nil { return domain.MarkupButton{}, domain.ErrButtonInvalid } - return domain.MarkupButton{Type: domain.MarkupButtonSwitchInline, Text: b.Text, Query: b.Query, SamePeer: b.SamePeer, PeerTypes: peerTypes, Style: style, IconCustomEmojiID: icon}, nil - case *tg.KeyboardButtonCopy: - return domain.MarkupButton{Type: domain.MarkupButtonCopy, Text: b.Text, CopyText: b.CopyText, Style: style, IconCustomEmojiID: icon}, nil + return domain.MarkupButton{Type: domain.MarkupButtonSwitchInline, Text: btn.Text, Query: t.Query, SamePeer: t.SamePeer, PeerTypes: peerTypes, Style: style, IconCustomEmojiID: icon}, nil + case *tg.InlineButtonTypeCopy: + return domain.MarkupButton{Type: domain.MarkupButtonCopy, Text: btn.Text, CopyText: t.CopyText, Style: style, IconCustomEmojiID: icon}, nil default: // webview/game/url_auth/request_*/switch_inline/buy 等 P3 未实现按钮类型。 return domain.MarkupButton{}, domain.ErrButtonTypeInvalid } } -func domainMarkupButtonStyle(btn tg.KeyboardButtonClass) (domain.MarkupButtonStyle, int64, error) { - style, ok := btn.GetStyle() +func domainMarkupButtonStyle(style tg.KeyboardButtonStyle, ok bool) (domain.MarkupButtonStyle, int64, error) { if !ok { return "", 0, nil } @@ -388,7 +391,7 @@ func tgReplyMarkup(m *domain.MessageReplyMarkup) tg.ReplyMarkupClass { case domain.MessageReplyMarkupKeyboard: rows := make([]tg.KeyboardButtonRow, 0, len(m.Keyboard)) for _, row := range m.Keyboard { - buttons := make([]tg.KeyboardButtonClass, 0, len(row)) + buttons := make([]tg.KeyboardButton, 0, len(row)) for _, btn := range row { buttons = append(buttons, tgReplyKeyboardButton(btn)) } @@ -415,93 +418,75 @@ func tgReplyMarkup(m *domain.MessageReplyMarkup) tg.ReplyMarkupClass { default: return nil } - rows := make([]tg.KeyboardButtonRow, 0, len(m.Inline)) + rows := make([]tg.KeyboardInlineButtonRow, 0, len(m.Inline)) for _, row := range m.Inline { - buttons := make([]tg.KeyboardButtonClass, 0, len(row)) + buttons := make([]tg.KeyboardInlineButton, 0, len(row)) for _, btn := range row { buttons = append(buttons, tgMarkupButton(btn)) } - rows = append(rows, tg.KeyboardButtonRow{Buttons: buttons}) + rows = append(rows, tg.KeyboardInlineButtonRow{Buttons: buttons}) } return &tg.ReplyInlineMarkup{Rows: rows} } -func tgMarkupButton(btn domain.MarkupButton) tg.KeyboardButtonClass { +func tgMarkupButton(btn domain.MarkupButton) tg.KeyboardInlineButton { + out := tg.KeyboardInlineButton{Text: btn.Text} + if style, ok := tgMarkupButtonStyle(btn); ok { + out.SetStyle(style) + } switch btn.Type { case domain.MarkupButtonURL: - out := &tg.KeyboardButtonURL{Text: btn.Text, URL: btn.URL} - if style, ok := tgMarkupButtonStyle(btn); ok { - out.SetStyle(style) - } - return out + out.Type = &tg.InlineButtonTypeURL{URL: btn.URL} case domain.MarkupButtonLoginURL: - out := &tg.KeyboardButtonURLAuth{Text: btn.Text, URL: btn.URL, ButtonID: btn.ButtonID} + t := &tg.InlineButtonTypeURLAuth{URL: btn.URL, ButtonID: btn.ButtonID} if btn.ForwardText != "" { - out.SetFwdText(btn.ForwardText) + t.SetFwdText(btn.ForwardText) } - if style, ok := tgMarkupButtonStyle(btn); ok { - out.SetStyle(style) - } - return out + out.Type = t case domain.MarkupButtonWebView: - out := &tg.KeyboardButtonWebView{Text: btn.Text, URL: btn.URL} - if style, ok := tgMarkupButtonStyle(btn); ok { - out.SetStyle(style) - } - return out + out.Type = &tg.InlineButtonTypeWebView{URL: btn.URL} case domain.MarkupButtonSwitchInline: - out := &tg.KeyboardButtonSwitchInline{Text: btn.Text, Query: btn.Query, SamePeer: btn.SamePeer} + t := &tg.InlineButtonTypeSwitchInline{Query: btn.Query, SamePeer: btn.SamePeer} if len(btn.PeerTypes) > 0 { - out.SetPeerTypes(tgPreparedInlinePeerTypes(btn.PeerTypes)) + t.SetPeerTypes(tgPreparedInlinePeerTypes(btn.PeerTypes)) } - if style, ok := tgMarkupButtonStyle(btn); ok { - out.SetStyle(style) - } - return out + out.Type = t case domain.MarkupButtonCopy: - out := &tg.KeyboardButtonCopy{Text: btn.Text, CopyText: btn.CopyText} - if style, ok := tgMarkupButtonStyle(btn); ok { - out.SetStyle(style) - } - return out + out.Type = &tg.InlineButtonTypeCopy{CopyText: btn.CopyText} default: // callback - out := &tg.KeyboardButtonCallback{Text: btn.Text, Data: btn.Data} + t := &tg.InlineButtonTypeCallback{Data: btn.Data} if btn.RequiresPassword { - out.SetRequiresPassword(true) + t.SetRequiresPassword(true) } - if style, ok := tgMarkupButtonStyle(btn); ok { - out.SetStyle(style) - } - return out + out.Type = t } + return out } -func tgReplyKeyboardButton(btn domain.MarkupButton) tg.KeyboardButtonClass { - var out tg.KeyboardButtonClass +func tgReplyKeyboardButton(btn domain.MarkupButton) tg.KeyboardButton { + out := tg.KeyboardButton{Text: btn.Text} switch btn.Type { case domain.MarkupButtonRequestPhone: - out = &tg.KeyboardButtonRequestPhone{Text: btn.Text} + out.Type = &tg.ButtonTypeRequestPhone{} case domain.MarkupButtonRequestLocation: - out = &tg.KeyboardButtonRequestGeoLocation{Text: btn.Text} + out.Type = &tg.ButtonTypeRequestGeoLocation{} case domain.MarkupButtonRequestPoll: - button := &tg.KeyboardButtonRequestPoll{Text: btn.Text} + t := &tg.ButtonTypeRequestPoll{} if btn.PollType == "quiz" { - button.SetQuiz(true) + t.SetQuiz(true) } else if btn.PollType == "regular" { - button.SetQuiz(false) + t.SetQuiz(false) } - out = button + out.Type = t case domain.MarkupButtonRequestPeer: - out = &tg.KeyboardButtonRequestPeer{Text: btn.Text, ButtonID: btn.ButtonID, PeerType: tgRequestPeerTypeWithFilter(btn.RequestPeerType, btn.RequestPeerFilter), MaxQuantity: btn.MaxQuantity} + out.Type = &tg.ButtonTypeRequestPeer{ButtonID: btn.ButtonID, PeerType: tgRequestPeerTypeWithFilter(btn.RequestPeerType, btn.RequestPeerFilter), MaxQuantity: btn.MaxQuantity} case domain.MarkupButtonSimpleWebView: - out = &tg.KeyboardButtonSimpleWebView{Text: btn.Text, URL: btn.URL} + out.Type = &tg.ButtonTypeSimpleWebView{URL: btn.URL} default: - out = &tg.KeyboardButton{Text: btn.Text} + out.Type = &tg.ButtonTypeDefault{} } if style, ok := tgMarkupButtonStyle(btn); ok { - if setter, ok := out.(interface{ SetStyle(tg.KeyboardButtonStyle) }); ok { - setter.SetStyle(style) - } + out.SetStyle(style) } return out } diff --git a/internal/rpc/convert_markup_test.go b/internal/rpc/convert_markup_test.go index 834ca2df..d2636264 100644 --- a/internal/rpc/convert_markup_test.go +++ b/internal/rpc/convert_markup_test.go @@ -9,22 +9,20 @@ import ( ) func TestReplyKeyboardTLDomainRoundTrip(t *testing.T) { + statusButton := tg.KeyboardButton{Text: "Status"} + style := tg.KeyboardButtonStyle{} + style.SetBgPrimary(true) + style.SetIcon(123456) + statusButton.SetStyle(style) in := &tg.ReplyKeyboardMarkup{ Resize: true, SingleUse: true, Selective: true, Persistent: true, Placeholder: "Choose", - Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButtonClass{ - &tg.KeyboardButton{Text: "Help"}, - func() *tg.KeyboardButton { - button := &tg.KeyboardButton{Text: "Status"} - style := tg.KeyboardButtonStyle{} - style.SetBgPrimary(true) - style.SetIcon(123456) - button.SetStyle(style) - return button - }(), + Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButton{ + {Text: "Help"}, + statusButton, }}}, } got, err := domainOutgoingReplyMarkupForSender(in, true) @@ -43,8 +41,9 @@ func TestReplyKeyboardTLDomainRoundTrip(t *testing.T) { if !ok || len(wire.Rows) != 1 || len(wire.Rows[0].Buttons) != 2 { t.Fatalf("wire markup = %#v", wire) } - if button, ok := wire.Rows[0].Buttons[1].(*tg.KeyboardButton); !ok || button.Text != "Status" { - t.Fatalf("second button = %#v", wire.Rows[0].Buttons[1]) + button := wire.Rows[0].Buttons[1] + if button.Text != "Status" { + t.Fatalf("second button = %#v", button) } else if style, ok := button.GetStyle(); !ok || !style.GetBgPrimary() || style.Icon != 123456 { t.Fatalf("second button style = %#v ok=%v", style, ok) } @@ -54,30 +53,32 @@ func TestReplyKeyboardTLDomainRoundTrip(t *testing.T) { } func TestInlineButtonStyleTLDomainRoundTrip(t *testing.T) { - button := &tg.KeyboardButtonCallback{Text: "Delete", Data: []byte("delete")} + button := tg.KeyboardInlineButton{Text: "Delete", Type: &tg.InlineButtonTypeCallback{Data: []byte("delete")}} style := tg.KeyboardButtonStyle{} style.SetBgDanger(true) button.SetStyle(style) - got, err := domainReplyMarkupForSender(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButtonClass{button}}}}, true) + got, err := domainReplyMarkupForSender(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardInlineButtonRow{{Buttons: []tg.KeyboardInlineButton{button}}}}, true) if err != nil { t.Fatalf("domainReplyMarkupForSender: %v", err) } if got.Inline[0][0].Style != domain.MarkupButtonStyleDanger { t.Fatalf("domain style = %#v", got.Inline[0][0]) } - wire := tgReplyMarkup(got).(*tg.ReplyInlineMarkup).Rows[0].Buttons[0].(*tg.KeyboardButtonCallback) + wire := tgReplyMarkup(got).(*tg.ReplyInlineMarkup).Rows[0].Buttons[0] if roundTrip, ok := wire.GetStyle(); !ok || !roundTrip.GetBgDanger() { t.Fatalf("wire style = %#v ok=%v", roundTrip, ok) } + if _, ok := wire.Type.(*tg.InlineButtonTypeCallback); !ok { + t.Fatalf("wire type = %#v", wire.Type) + } } func TestLoginURLButtonTLDomainProjection(t *testing.T) { - button := &tg.InputKeyboardButtonURLAuth{ - Text: "Log in", URL: "https://example.com/login", Bot: &tg.InputUser{UserID: 9001, AccessHash: 77}, - } - button.SetRequestWriteAccess(true) - button.SetFwdText("Open login") - markup, err := domainReplyMarkupForSender(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButtonClass{button}}}}, true) + buttonType := &tg.InputInlineButtonTypeURLAuth{URL: "https://example.com/login", Bot: &tg.InputUser{UserID: 9001, AccessHash: 77}} + buttonType.SetRequestWriteAccess(true) + buttonType.SetFwdText("Open login") + button := tg.KeyboardInlineButton{Text: "Log in", Type: buttonType} + markup, err := domainReplyMarkupForSender(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardInlineButtonRow{{Buttons: []tg.KeyboardInlineButton{button}}}}, true) if err != nil { t.Fatal(err) } @@ -85,8 +86,9 @@ func TestLoginURLButtonTLDomainProjection(t *testing.T) { if got.Type != domain.MarkupButtonLoginURL || got.LoginBotUserID != 9001 || !got.RequestWriteAccess || got.ForwardText != "Open login" || got.ButtonID != 0 { t.Fatalf("domain login_url = %#v", got) } - wire, ok := tgReplyMarkup(markup).(*tg.ReplyInlineMarkup).Rows[0].Buttons[0].(*tg.KeyboardButtonURLAuth) - if !ok || wire.Text != "Log in" || wire.URL != "https://example.com/login" || wire.ButtonID != 0 || wire.FwdText != "Open login" { + wire := tgReplyMarkup(markup).(*tg.ReplyInlineMarkup).Rows[0].Buttons[0] + wireType, ok := wire.Type.(*tg.InlineButtonTypeURLAuth) + if !ok || wire.Text != "Log in" || wireType.URL != "https://example.com/login" || wireType.ButtonID != 0 || wireType.FwdText != "Open login" { t.Fatalf("wire login_url = %#v", wire) } } @@ -112,7 +114,7 @@ func TestReplyKeyboardHideAndForceReplyTLDomainRoundTrip(t *testing.T) { func TestReplyKeyboardRequestPhoneTLDomainRoundTrip(t *testing.T) { markup, err := domainOutgoingReplyMarkupForSender(&tg.ReplyKeyboardMarkup{Rows: []tg.KeyboardButtonRow{{ - Buttons: []tg.KeyboardButtonClass{&tg.KeyboardButtonRequestPhone{Text: "Share phone"}}, + Buttons: []tg.KeyboardButton{{Text: "Share phone", Type: &tg.ButtonTypeRequestPhone{}}}, }}}, true) if err != nil || markup == nil || len(markup.Keyboard) != 1 || len(markup.Keyboard[0]) != 1 || markup.Keyboard[0][0].Type != domain.MarkupButtonRequestPhone { @@ -122,7 +124,7 @@ func TestReplyKeyboardRequestPhoneTLDomainRoundTrip(t *testing.T) { if !ok || len(wire.Rows) != 1 || len(wire.Rows[0].Buttons) != 1 { t.Fatalf("request_phone wire = %#v", wire) } - if _, ok := wire.Rows[0].Buttons[0].(*tg.KeyboardButtonRequestPhone); !ok { + if _, ok := wire.Rows[0].Buttons[0].Type.(*tg.ButtonTypeRequestPhone); !ok { t.Fatalf("request_phone button = %#v", wire.Rows[0].Buttons[0]) } if _, err := domainReplyMarkupForSender(&tg.ReplyKeyboardHide{}, true); err == nil { @@ -138,9 +140,9 @@ func TestReplyKeyboardRequestPeerFiltersTLDomainRoundTrip(t *testing.T) { chatType.SetHasUsername(false) chatType.SetForum(true) chatType.SetUserAdminRights(tg.ChatAdminRights{DeleteMessages: true, ManageTopics: true}) - in := &tg.ReplyKeyboardMarkup{Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButtonClass{ - &tg.KeyboardButtonRequestPeer{Text: "Premium person", ButtonID: 1, PeerType: userType, MaxQuantity: 2}, - &tg.KeyboardButtonRequestPeer{Text: "Forum", ButtonID: 2, PeerType: chatType, MaxQuantity: 1}, + in := &tg.ReplyKeyboardMarkup{Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButton{ + {Text: "Premium person", Type: &tg.ButtonTypeRequestPeer{ButtonID: 1, PeerType: userType, MaxQuantity: 2}}, + {Text: "Forum", Type: &tg.ButtonTypeRequestPeer{ButtonID: 2, PeerType: chatType, MaxQuantity: 1}}, }}}} markup, err := domainOutgoingReplyMarkupForSender(in, true) if err != nil { @@ -157,14 +159,14 @@ func TestReplyKeyboardRequestPeerFiltersTLDomainRoundTrip(t *testing.T) { t.Fatalf("chat filter = %#v", chatFilter) } wire := tgReplyMarkup(markup).(*tg.ReplyKeyboardMarkup) - wireUser := wire.Rows[0].Buttons[0].(*tg.KeyboardButtonRequestPeer).PeerType.(*tg.RequestPeerTypeUser) + wireUser := wire.Rows[0].Buttons[0].Type.(*tg.ButtonTypeRequestPeer).PeerType.(*tg.RequestPeerTypeUser) if bot, ok := wireUser.GetBot(); !ok || bot { t.Fatalf("wire user bot=%v ok=%v", bot, ok) } if premium, ok := wireUser.GetPremium(); !ok || !premium { t.Fatalf("wire user premium=%v ok=%v", premium, ok) } - wireChat := wire.Rows[0].Buttons[1].(*tg.KeyboardButtonRequestPeer).PeerType.(*tg.RequestPeerTypeChat) + wireChat := wire.Rows[0].Buttons[1].Type.(*tg.ButtonTypeRequestPeer).PeerType.(*tg.RequestPeerTypeChat) if !wireChat.Creator || !wireChat.BotParticipant { t.Fatalf("wire chat = %#v", wireChat) } @@ -177,9 +179,12 @@ func TestReplyKeyboardRequestPeerFiltersTLDomainRoundTrip(t *testing.T) { } func TestInputRequestPeerButtonPreservesRequestedMetadata(t *testing.T) { - button := &tg.InputKeyboardButtonRequestPeer{ - NameRequested: true, UsernameRequested: true, PhotoRequested: true, - Text: "Share", ButtonID: 99, PeerType: &tg.RequestPeerTypeUser{}, MaxQuantity: 3, + button := tg.KeyboardButton{ + Text: "Share", + Type: &tg.InputButtonTypeRequestPeer{ + NameRequested: true, UsernameRequested: true, PhotoRequested: true, + ButtonID: 99, PeerType: &tg.RequestPeerTypeUser{}, MaxQuantity: 3, + }, } got, err := domainRequestedButtonFromTG(1001, nil, button) if err != nil { diff --git a/internal/rpc/convert_media.go b/internal/rpc/convert_media.go index 9920c2ce..3995959a 100644 --- a/internal/rpc/convert_media.go +++ b/internal/rpc/convert_media.go @@ -367,6 +367,11 @@ func tgPhotoSizes(sizes []domain.PhotoSize) []tg.PhotoSizeClass { func tgPhotoSize(s domain.PhotoSize) tg.PhotoSizeClass { switch s.Kind { case domain.PhotoSizeKindDefault: + if s.W <= 0 || s.H <= 0 { + // A zero-dimension photoSize is malformed and divides to a crash on + // the client; dropping it lets clients fall back to another size. + return nil + } return &tg.PhotoSize{Type: s.Type, W: s.W, H: s.H, Size: s.Size} case domain.PhotoSizeKindStripped: return &tg.PhotoStrippedSize{Type: s.Type, Bytes: s.Bytes} @@ -464,12 +469,47 @@ func compactPhotoSizeClasses(in []tg.PhotoSizeClass) []tg.PhotoSizeClass { return out } +// stickerCanvasSize is the square canvas a sticker / custom emoji is laid out on. +// It is the fallback dimension when stored metadata carries a zero, which clients +// divide by while sizing the render (a 0 there crashes Telegram Desktop). +const stickerCanvasSize = 512 + +func stickerLikeMime(mimeType string) bool { + switch mimeType { + case mimeApplicationXTGSticker, "image/webp", "video/webm": + return true + default: + return false + } +} + +// safeMediaDimension replaces a non-positive width/height with a usable value: +// the sticker canvas for sticker-like documents, otherwise 1, so no projected +// attribute ever carries a zero a client would divide by. +func safeMediaDimension(v int, mimeType string) int { + if v > 0 { + return v + } + if stickerLikeMime(mimeType) { + return stickerCanvasSize + } + return 1 +} + func tgDocumentAttributes(mimeType string, attrs []domain.DocumentAttribute) []tg.DocumentAttributeClass { out := make([]tg.DocumentAttributeClass, 0, len(attrs)) for _, a := range attrs { switch a.Kind { case domain.DocAttrImageSize: - out = append(out, &tg.DocumentAttributeImageSize{W: a.W, H: a.H}) + if (a.W <= 0 || a.H <= 0) && !stickerLikeMime(mimeType) { + // Malformed size on a plain image: drop it rather than emit a zero. + // Clients cope with a missing imageSize; a zero one crashes them. + continue + } + out = append(out, &tg.DocumentAttributeImageSize{ + W: safeMediaDimension(a.W, mimeType), + H: safeMediaDimension(a.H, mimeType), + }) case domain.DocAttrAnimated: if mimeType == mimeApplicationXTGSticker { continue @@ -487,8 +527,8 @@ func tgDocumentAttributes(mimeType string, attrs []domain.DocumentAttribute) []t SupportsStreaming: a.SupportsStreaming, Nosound: a.NoSound, Duration: a.Duration, - W: a.W, - H: a.H, + W: safeMediaDimension(a.W, mimeType), + H: safeMediaDimension(a.H, mimeType), } if a.VideoCodec != "" { video.SetVideoCodec(a.VideoCodec) diff --git a/internal/rpc/errors.go b/internal/rpc/errors.go index 564bb558..197ca0d0 100644 --- a/internal/rpc/errors.go +++ b/internal/rpc/errors.go @@ -291,6 +291,7 @@ func scoreInvalidErr() error { return tgerr.New(400, "SCORE_INVALID") } func sessionPasswordNeededErr() error { return tgerr.New(401, "SESSION_PASSWORD_NEEDED") } func passwordHashInvalidErr() error { return tgerr.New(400, "PASSWORD_HASH_INVALID") } +func passwordMissingErr() error { return tgerr.New(400, "PASSWORD_MISSING") } func srpIDInvalidErr() error { return tgerr.New(400, "SRP_ID_INVALID") } func srpPasswordChangedErr() error { return tgerr.New(400, "SRP_PASSWORD_CHANGED") } func newSettingsInvalidErr() error { return tgerr.New(400, "NEW_SETTINGS_INVALID") } @@ -468,6 +469,8 @@ func passwordErr(err error) error { switch { case errors.Is(err, domain.ErrPasswordHashInvalid): return passwordHashInvalidErr() + case errors.Is(err, domain.ErrPasswordMissing): + return passwordMissingErr() case errors.Is(err, domain.ErrSRPIDInvalid): return srpIDInvalidErr() case errors.Is(err, domain.ErrSRPPasswordChanged): diff --git a/internal/rpc/forum_reply_topic_rpc_test.go b/internal/rpc/forum_reply_topic_rpc_test.go new file mode 100644 index 00000000..8ef2dad7 --- /dev/null +++ b/internal/rpc/forum_reply_topic_rpc_test.go @@ -0,0 +1,120 @@ +package rpc + +import ( + "context" + "testing" + + "github.com/iamxvbaba/td/clock" + "github.com/iamxvbaba/td/tg" + "go.uber.org/zap/zaptest" + + appchannels "telesrv/internal/app/channels" + appusers "telesrv/internal/app/users" + "telesrv/internal/domain" + "telesrv/internal/store/memory" +) + +// A reply inside a forum must inherit the *target's* topic, never the target's +// own message id. Regression: replying to a General message produced +// reply_to_top_id = , a topic that no client can resolve, so +// the reply vanished from every topic view and reply-jump said "doesn't exist". +func TestForumReplyInheritsTargetTopic(t *testing.T) { + ctx := context.Background() + userStore := memory.NewUserStore() + owner, _ := userStore.Create(ctx, domain.User{AccessHash: 91, Phone: "15550009101", FirstName: "Owner"}) + channelStore := memory.NewChannelStore() + r := New(Config{}, Deps{ + Users: appusers.NewService(userStore), + Channels: appchannels.NewService(channelStore), + }, zaptest.NewLogger(t), clock.System) + ownerCtx := WithUserID(ctx, owner.ID) + + created, err := r.onChannelsCreateChannel(ownerCtx, &tg.ChannelsCreateChannelRequest{Title: "Forum", Megagroup: true}) + if err != nil { + t.Fatalf("create channel: %v", err) + } + channel := created.(*tg.Updates).Chats[0].(*tg.Channel) + input := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash} + peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash} + if _, err := r.onChannelsToggleForum(ownerCtx, &tg.ChannelsToggleForumRequest{Channel: input, Enabled: true, Tabs: true}); err != nil { + t.Fatalf("toggle forum: %v", err) + } + topicUpd, err := r.onMessagesCreateForumTopic(ownerCtx, &tg.MessagesCreateForumTopicRequest{ + Peer: peer, Title: "Test", IconColor: domain.DefaultForumTopicIconColor, RandomID: 9101001, + }) + if err != nil { + t.Fatalf("create topic: %v", err) + } + testTopicID := forumTopicRootMessageID(t, topicUpd, "Test") + + send := func(text string, randomID int64, reply *tg.InputReplyToMessage) *tg.Message { + req := &tg.MessagesSendMessageRequest{Peer: peer, Message: text, RandomID: randomID} + if reply != nil { + req.SetReplyTo(reply) + } + upd, err := r.onMessagesSendMessage(ownerCtx, req) + if err != nil { + t.Fatalf("send %q: %v", text, err) + } + for _, u := range upd.(*tg.Updates).Updates { + if nm, ok := u.(*tg.UpdateNewChannelMessage); ok { + if m, ok := nm.Message.(*tg.Message); ok && m.Message == text { + return m + } + } + } + t.Fatalf("no new message for %q in %+v", text, upd) + return nil + } + topID := func(m *tg.Message) int { + h, ok := m.ReplyTo.(*tg.MessageReplyHeader) + if !ok { + t.Fatalf("message %d has reply header %T, want *MessageReplyHeader", m.ID, m.ReplyTo) + } + id, _ := h.GetReplyToTopID() + if !h.ForumTopic { + t.Fatalf("message %d reply header missing forum_topic flag: %+v", m.ID, h) + } + return id + } + + // A plain General message (no reply header). + g1 := send("g1", 9101002, nil) + + // Reply to it -> topic must be General (1), not g1.ID. + r1 := send("r1", 9101003, &tg.InputReplyToMessage{ReplyToMsgID: g1.ID}) + if got := topID(r1); got != domain.ForumGeneralTopicID { + t.Fatalf("reply to a General message: reply_to_top_id = %d, want %d (General), not the target id %d", + got, domain.ForumGeneralTopicID, g1.ID) + } + + // Reply again, this time the client also passes top_msg_id: 1 (General). + // Previously this was rejected because General has no channel_forum_topics row. + replyWithTop := &tg.InputReplyToMessage{ReplyToMsgID: g1.ID} + replyWithTop.SetTopMsgID(domain.ForumGeneralTopicID) + r2 := send("r2", 9101004, replyWithTop) + if got := topID(r2); got != domain.ForumGeneralTopicID { + t.Fatalf("reply with top_msg_id=1: reply_to_top_id = %d, want %d", got, domain.ForumGeneralTopicID) + } + + // Post directly into the "Test" topic, then reply to a plain message there. + tInTopic := &tg.InputReplyToMessage{ReplyToMsgID: 0} + tInTopic.SetTopMsgID(testTopicID) + m1 := send("t1", 9101005, tInTopic) + if got := topID(m1); got != testTopicID { + t.Fatalf("message in Test topic: reply_to_top_id = %d, want %d", got, testTopicID) + } + rt := send("rt", 9101006, &tg.InputReplyToMessage{ReplyToMsgID: m1.ID}) + if got := topID(rt); got != testTopicID { + t.Fatalf("reply inside Test topic: reply_to_top_id = %d, want %d (topic), not %d", got, testTopicID, m1.ID) + } + + // Replying to a General message while claiming a mismatched topic is rejected. + bad := &tg.InputReplyToMessage{ReplyToMsgID: g1.ID} + bad.SetTopMsgID(testTopicID) + req := &tg.MessagesSendMessageRequest{Peer: peer, Message: "bad", RandomID: 9101007} + req.SetReplyTo(bad) + if _, err := r.onMessagesSendMessage(ownerCtx, req); err == nil { + t.Fatal("reply with a topic id that doesn't match the target's topic was accepted") + } +} diff --git a/internal/rpc/forum_topics_preview_rpc_test.go b/internal/rpc/forum_topics_preview_rpc_test.go new file mode 100644 index 00000000..3dd6b8ba --- /dev/null +++ b/internal/rpc/forum_topics_preview_rpc_test.go @@ -0,0 +1,133 @@ +package rpc + +import ( + "context" + "testing" + + "github.com/iamxvbaba/td/clock" + "github.com/iamxvbaba/td/tg" + "go.uber.org/zap/zaptest" + + appchannels "telesrv/internal/app/channels" + appusers "telesrv/internal/app/users" + "telesrv/internal/domain" + "telesrv/internal/store/memory" +) + +// A public forum's topic list is browsable before joining, like its history. +// Regression: getForumTopics used the member-only access path and returned +// CHANNEL_PRIVATE / an empty list to non-members, so the topic list (and even +// General) was invisible until they joined. +func TestGetForumTopicsVisibleToPublicNonMember(t *testing.T) { + ctx := context.Background() + userStore := memory.NewUserStore() + owner, _ := userStore.Create(ctx, domain.User{AccessHash: 81, Phone: "15550008101", FirstName: "Owner"}) + outsider, _ := userStore.Create(ctx, domain.User{AccessHash: 82, Phone: "15550008102", FirstName: "Outsider"}) + channelStore := memory.NewChannelStore() + channelSvc := appchannels.NewService(channelStore) + r := New(Config{}, Deps{ + Users: appusers.NewService(userStore), + Channels: channelSvc, + }, zaptest.NewLogger(t), clock.System) + + ownerCtx := WithUserID(ctx, owner.ID) + outsiderCtx := WithUserID(ctx, outsider.ID) + + created, err := r.onChannelsCreateChannel(ownerCtx, &tg.ChannelsCreateChannelRequest{Title: "Public Forum", Megagroup: true}) + if err != nil { + t.Fatalf("create channel: %v", err) + } + channel := created.(*tg.Updates).Chats[0].(*tg.Channel) + input := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash} + forumPeer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash} + + if _, err := r.onChannelsToggleForum(ownerCtx, &tg.ChannelsToggleForumRequest{Channel: input, Enabled: true, Tabs: true}); err != nil { + t.Fatalf("toggle forum: %v", err) + } + if _, err := channelSvc.UpdateUsername(ctx, owner.ID, domain.UpdateChannelUsernameRequest{ + ChannelID: channel.ID, + Username: "publicforum", + }); err != nil { + t.Fatalf("set channel username: %v", err) + } + if _, err := r.onMessagesCreateForumTopic(ownerCtx, &tg.MessagesCreateForumTopicRequest{ + Peer: forumPeer, + Title: "Test", + IconColor: domain.DefaultForumTopicIconColor, + RandomID: 8101001, + }); err != nil { + t.Fatalf("create forum topic: %v", err) + } + + res, err := r.onMessagesGetForumTopics(outsiderCtx, &tg.MessagesGetForumTopicsRequest{ + Peer: forumPeer, + Limit: 100, + }) + if err != nil { + t.Fatalf("getForumTopics as non-member: %v", err) + } + titles := map[string]bool{} + testTopicID := 0 + for _, tc := range res.Topics { + switch topic := tc.(type) { + case *tg.ForumTopic: + titles[topic.Title] = true + if topic.Title == "Test" { + testTopicID = topic.ID + } + case *tg.ForumTopicDeleted: + } + } + if !titles["General"] { + t.Fatalf("non-member did not see the General topic: %+v", res.Topics) + } + if !titles["Test"] { + t.Fatalf("non-member did not see the Test topic: %+v", res.Topics) + } + + // A non-member can also read the replies inside a topic (preview), the same + // way ListChannelHistory lets them preview a public group's flat history. + if testTopicID == 0 { + t.Fatal("no Test topic id to open") + } + if _, err := r.onMessagesGetReplies(outsiderCtx, &tg.MessagesGetRepliesRequest{ + Peer: forumPeer, + MsgID: testTopicID, + Limit: 20, + }); err != nil { + t.Fatalf("getReplies as non-member of a public forum: %v", err) + } + + // The forum's own channel must come back with left=true so the client still + // offers a Join button instead of treating the forum as already joined. + var forumChat *tg.Channel + for _, c := range res.Chats { + if ch, ok := c.(*tg.Channel); ok && ch.ID == channel.ID { + forumChat = ch + } + } + if forumChat == nil { + t.Fatalf("forum channel missing from getForumTopics chats: %+v", res.Chats) + } + if !forumChat.Left { + t.Fatalf("non-member forum chat = %#v, want left=true", forumChat) + } + + // A private forum still refuses a non-member. + priv, err := r.onChannelsCreateChannel(ownerCtx, &tg.ChannelsCreateChannelRequest{Title: "Private Forum", Megagroup: true}) + if err != nil { + t.Fatalf("create private channel: %v", err) + } + privCh := priv.(*tg.Updates).Chats[0].(*tg.Channel) + privInput := &tg.InputChannel{ChannelID: privCh.ID, AccessHash: privCh.AccessHash} + privPeer := &tg.InputPeerChannel{ChannelID: privCh.ID, AccessHash: privCh.AccessHash} + if _, err := r.onChannelsToggleForum(ownerCtx, &tg.ChannelsToggleForumRequest{Channel: privInput, Enabled: true, Tabs: true}); err != nil { + t.Fatalf("toggle private forum: %v", err) + } + if _, err := r.onMessagesGetForumTopics(outsiderCtx, &tg.MessagesGetForumTopicsRequest{Peer: privPeer, Limit: 100}); err == nil { + t.Fatal("non-member read a private forum's topic list") + } + if _, err := r.onMessagesGetReplies(outsiderCtx, &tg.MessagesGetRepliesRequest{Peer: privPeer, MsgID: 1, Limit: 20}); err == nil { + t.Fatal("non-member read a private forum topic's replies") + } +} diff --git a/internal/rpc/help.go b/internal/rpc/help.go index 22baf36a..67cf33c4 100644 --- a/internal/rpc/help.go +++ b/internal/rpc/help.go @@ -26,7 +26,7 @@ func (r *Router) registerHelp(d *tlprofile.Dispatcher) { }, nil }) registerRPC[*tg.HelpGetInviteTextRequest](d, tlprofile.SemanticMethodHelpGetInviteText, func(ctx context.Context, layerRequest *tg.HelpGetInviteTextRequest) (any, error) { - return &tg.HelpInviteText{Message: "Join me on " + branding.ProductName + "."}, nil + return &tg.HelpInviteText{Message: "Join me on " + branding.ProductName() + "."}, nil }) registerRPC[*tg.HelpSaveAppLogRequest](d, tlprofile.SemanticMethodHelpSaveAppLog, func(ctx context.Context, _ *tg.HelpSaveAppLogRequest) (any, error) { return r.onHelpSaveAppLog(ctx) @@ -186,7 +186,7 @@ func (r *Router) onHelpDismissSuggestion(ctx context.Context, req *tg.HelpDismis // dead payment URLs. All six TL fields are mandatory. func (r *Router) onHelpGetPremiumPromo(ctx context.Context) (*tg.HelpPremiumPromo, error) { promo := &tg.HelpPremiumPromo{ - StatusText: branding.PremiumName + " is not active on this account.", + StatusText: branding.PremiumName() + " is not active on this account.", StatusEntities: []tg.MessageEntityClass{}, VideoSections: []string{}, Videos: []tg.DocumentClass{}, @@ -215,7 +215,7 @@ func (r *Router) onHelpGetPremiumPromo(ctx context.Context) (*tg.HelpPremiumProm } if u.PremiumActiveAt(r.clock.Now().Unix()) { until := time.Unix(int64(u.PremiumUntil), 0) - promo.StatusText = branding.PremiumName + " is active until " + until.Format("2006-01-02") + "." + promo.StatusText = branding.PremiumName() + " is active until " + until.Format("2006-01-02") + "." } if r.deps.PremiumPromo != nil { catalog, found, err := r.deps.PremiumPromo.PremiumPromo(ctx) diff --git a/internal/rpc/messages_bot_no_state.go b/internal/rpc/messages_bot_no_state.go index 8cedef6e..ad82ff36 100644 --- a/internal/rpc/messages_bot_no_state.go +++ b/internal/rpc/messages_bot_no_state.go @@ -341,7 +341,10 @@ func (r *Router) mentionedUserIDsFromDomainMessage(ctx context.Context, currentU for _, username := range extractMentionUsernames(message, domain.MaxChannelMentionRecipients-len(out)) { user, found, err := identity.ResolveUsername(ctx, currentUserID, username) if err != nil { - return nil, internalErr() + if mentionResolveFatal(err) { + return nil, internalErr() + } + continue } if found { add(user.ID) diff --git a/internal/rpc/messages_bot_no_state_rpc_test.go b/internal/rpc/messages_bot_no_state_rpc_test.go index 03a16c2d..31d4a2d9 100644 --- a/internal/rpc/messages_bot_no_state_rpc_test.go +++ b/internal/rpc/messages_bot_no_state_rpc_test.go @@ -99,8 +99,8 @@ func TestMessagesEditInlineBotMessageEditsPrivateInlineMessage(t *testing.T) { editReq := &tg.MessagesEditInlineBotMessageRequest{ID: msgID} editReq.SetMessage("after edit") - editReq.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{ - Buttons: []tg.KeyboardButtonClass{&tg.KeyboardButtonCallback{Text: "done", Data: []byte("v2")}}, + editReq.SetReplyMarkup(&tg.ReplyInlineMarkup{Rows: []tg.KeyboardInlineButtonRow{{ + Buttons: []tg.KeyboardInlineButton{{Text: "done", Type: &tg.InlineButtonTypeCallback{Data: []byte("v2")}}}, }}}) if ok, err := f.router.onMessagesEditInlineBotMessage(botCtx, editReq); err != nil || !ok { t.Fatalf("edit inline bot message = %v,%v, want true,nil", ok, err) @@ -291,8 +291,8 @@ func inlineArticleResultWithCallbackMarkup(id, message, button string, data []by Title: id, SendMessage: &tg.InputBotInlineMessageText{ Message: message, - ReplyMarkup: &tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{ - Buttons: []tg.KeyboardButtonClass{&tg.KeyboardButtonCallback{Text: button, Data: data}}, + ReplyMarkup: &tg.ReplyInlineMarkup{Rows: []tg.KeyboardInlineButtonRow{{ + Buttons: []tg.KeyboardInlineButton{{Text: button, Type: &tg.InlineButtonTypeCallback{Data: data}}}, }}}, }, } diff --git a/internal/rpc/messages_forum.go b/internal/rpc/messages_forum.go index 4a0e939a..03702495 100644 --- a/internal/rpc/messages_forum.go +++ b/internal/rpc/messages_forum.go @@ -474,12 +474,37 @@ func (r *Router) forumTopicsResponse(ctx context.Context, userID int64, view dom Count: count, Topics: topics, Messages: messages, - Chats: tgChannels(userID, channels), + Chats: r.forumTopicsChats(userID, view, channels), Users: r.tgUsersForIDs(ctx, userID, userIDs), Pts: view.Channel.Pts, }) } +// forumTopicsChats projects the forum's own channel with the viewer's member +// state (so a non-member preview carries left=true and the client still shows a +// Join button) and every other referenced channel as a min chat. Rendering the +// primary as a bare min chat lets a client that has no other object for the +// channel treat the forum as already joined. +func (r *Router) forumTopicsChats(userID int64, view domain.ChannelView, channels []domain.Channel) []tg.ChatClass { + if view.Channel.ID == 0 { + return tgChannels(userID, channels) + } + chats := make([]tg.ChatClass, 0, len(channels)) + chats = append(chats, tgChannelChatForView(userID, view)) + seen := map[int64]struct{}{view.Channel.ID: {}} + for _, extra := range channels { + if extra.ID == 0 { + continue + } + if _, dup := seen[extra.ID]; dup { + continue + } + seen[extra.ID] = struct{}{} + chats = append(chats, tgChannelChatMin(userID, extra)) + } + return chats +} + func tgForumGeneralTopic(viewerUserID int64, view domain.ChannelView, topic domain.ChannelForumTopic) *tg.ForumTopic { return &tg.ForumTopic{ My: view.Channel.CreatorUserID == viewerUserID && viewerUserID != 0, diff --git a/internal/rpc/messages_send.go b/internal/rpc/messages_send.go index 51cbc892..ec8d6880 100644 --- a/internal/rpc/messages_send.go +++ b/internal/rpc/messages_send.go @@ -437,7 +437,10 @@ func (r *Router) mentionedUserIDsFromMessage(ctx context.Context, currentUserID for _, username := range extractMentionUsernames(message, domain.MaxChannelMentionRecipients-len(out)) { user, found, err := identity.ResolveUsername(ctx, currentUserID, username) if err != nil { - return nil, internalErr() + if mentionResolveFatal(err) { + return nil, internalErr() + } + continue } if found { add(user.ID) @@ -450,6 +453,17 @@ func (r *Router) mentionedUserIDsFromMessage(ctx context.Context, currentUserID return out, nil } +// mentionResolveFatal reports whether a ResolveUsername error while scanning +// message text for @mentions should abort the send. A syntactically invalid or +// unoccupied @token is not a failure: real Telegram sends the message, renders +// the token as a mention, and only fails to open it when the reader taps it. +// Only an unexpected (storage) error aborts the RPC. +func mentionResolveFatal(err error) bool { + return err != nil && + !errors.Is(err, domain.ErrUsernameInvalid) && + !errors.Is(err, domain.ErrUsernameNotOccupied) +} + func extractMentionUsernames(message string, limit int) []string { if limit <= 0 || message == "" { return nil diff --git a/internal/rpc/payments.go b/internal/rpc/payments.go index f564b4d5..bd57be06 100644 --- a/internal/rpc/payments.go +++ b/internal/rpc/payments.go @@ -12,9 +12,10 @@ import ( // registerPayments 注册 payments.* RPC。telesrv 不实现 Stars/Star Gift 经济: // 大部分曾经的 Stars/gift 动作 RPC 已不再注册;仍注册的几个只读状态 RPC -// (getStarsStatus/Subscriptions/Transactions/RevenueStats)返回固定空值, -// 因为部分客户端界面会无条件加载它们——错误返回会导致界面卡死或崩溃, -// 空值则让界面正常渲染成"没有 Stars"。 +// (getStarsStatus/Subscriptions/Transactions/RevenueStats、getStarGifts、 +// getSavedStarGifts)返回固定空值,因为部分客户端界面会无条件加载它们—— +// 错误返回会导致界面卡死或崩溃,且 tdesktop 对 500 会疯狂重试,空值则让界面 +// 正常渲染成"没有 Stars / 没有礼物"。 func (r *Router) registerPayments(d *tlprofile.Dispatcher) { registerRPC[*tg.PaymentsCanPurchaseStoreRequest](d, tlprofile.SemanticMethodPaymentsCanPurchaseStore, func(ctx context.Context, req *tg.PaymentsCanPurchaseStoreRequest) (any, error) { return r.onPaymentsCanPurchaseStore(ctx, req) @@ -51,7 +52,22 @@ func (r *Router) registerPayments(d *tlprofile.Dispatcher) { registerRPC[*tg.PaymentsGetStarsRevenueStatsRequest](d, tlprofile.SemanticMethodPaymentsGetStarsRevenueStats, func(ctx context.Context, req *tg.PaymentsGetStarsRevenueStatsRequest) (any, error) { return r.onPaymentsGetStarsRevenueStats(ctx, req) }) - + // Star Gift catalogue / profile gift list: telesrv has no gift economy, but + // tdesktop polls both on startup and retries hard on an error (a 500 turns + // into a request storm). Answer with a valid empty list so the client stops. + registerRPC[*tg.PaymentsGetStarGiftsRequest](d, tlprofile.SemanticMethodPaymentsGetStarGifts, func(ctx context.Context, _ *tg.PaymentsGetStarGiftsRequest) (any, error) { + if _, _, err := r.currentUserID(ctx); err != nil { + return nil, internalErr() + } + return &tg.PaymentsStarGifts{Gifts: []tg.StarGiftClass{}, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}}, nil + }) + registerRPC[*tg.PaymentsGetSavedStarGiftsRequest](d, tlprofile.SemanticMethodPaymentsGetSavedStarGifts, func(ctx context.Context, _ *tg.PaymentsGetSavedStarGiftsRequest) (any, error) { + if _, _, err := r.currentUserID(ctx); err != nil { + return nil, internalErr() + } + // No next_offset: an empty page must be terminal, not an infinite scroll. + return &tg.PaymentsSavedStarGifts{Gifts: []tg.SavedStarGift{}, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}}, nil + }) } func (r *Router) onPaymentsCanPurchaseStore(ctx context.Context, _ *tg.PaymentsCanPurchaseStoreRequest) (bool, error) { diff --git a/internal/rpc/presence.go b/internal/rpc/presence.go index 92fb5a8d..659e51f3 100644 --- a/internal/rpc/presence.go +++ b/internal/rpc/presence.go @@ -531,6 +531,13 @@ func (r *Router) withUserPresence(u domain.User) domain.User { if u.Bot { return u } + // 已注销账号同理不参与 presence:tgUser/tgSelfUser 对 Deleted 用户直接短路输出 + // 精简 tombstone,从不读取 Status,这里覆盖与否本应无观测差异——但只靠那一层 + // 短路里应外合:任何上游把 Deleted 弄丢的 bug(例如缓存往返丢字段)都会让这里 + // 覆盖出的 Status 变成可见的“最近上线”,掩盖真正的 bug 而不是让它更早炸出来。 + if u.Deleted { + return u + } u.Status = r.userPresenceStatusForUser(u) return u } diff --git a/internal/rpc/router_dispatch_test.go b/internal/rpc/router_dispatch_test.go index a2fb3a2a..6d56b00c 100644 --- a/internal/rpc/router_dispatch_test.go +++ b/internal/rpc/router_dispatch_test.go @@ -1290,6 +1290,8 @@ func TestTDesktopStartupRPCsEncode(t *testing.T) { {name: "payments.canPurchaseStore", req: &tg.PaymentsCanPurchaseStoreRequest{Purpose: &tg.InputStorePaymentStarsTopup{Stars: 1000, Currency: "USD", Amount: 99}}}, {name: "payments.getStarsStatus", req: &tg.PaymentsGetStarsStatusRequest{Peer: &tg.InputPeerSelf{}}}, {name: "payments.getStarsSubscriptions", req: &tg.PaymentsGetStarsSubscriptionsRequest{Peer: &tg.InputPeerSelf{}}}, + {name: "payments.getStarGifts", req: &tg.PaymentsGetStarGiftsRequest{}}, + {name: "payments.getSavedStarGifts", req: &tg.PaymentsGetSavedStarGiftsRequest{Peer: &tg.InputPeerSelf{}, Limit: 20}}, {name: "updates.getDifference", req: &tg.UpdatesGetDifferenceRequest{}}, {name: "users.getFullUser", req: &tg.UsersGetFullUserRequest{ID: &tg.InputUserSelf{}}}, {name: "users.getRequirementsToContact", req: &tg.UsersGetRequirementsToContactRequest{ID: []tg.InputUserClass{&tg.InputUserSelf{}}}}, diff --git a/internal/rpc/rpc_projection_cache.go b/internal/rpc/rpc_projection_cache.go index f81a6ad5..f96abf4f 100644 --- a/internal/rpc/rpc_projection_cache.go +++ b/internal/rpc/rpc_projection_cache.go @@ -290,6 +290,24 @@ func (r *Router) invalidateRPCProjectionForPeer(ownerUserID int64, peer domain.P } } +// invalidateChannelMembershipProjection drops the cached channels.getFullChannel +// projection for each user whose membership in channelID just changed (join, +// leave, invite, request approval). Without it a client that polls +// channels.getFullChannel right after channels.leaveChannel keeps getting a +// projection that still shows it as an active member (left=false) until the +// entry's TTL lapses, so it keeps an open compose box even though sends are +// already rejected with CHANNEL_PRIVATE. +func (r *Router) invalidateChannelMembershipProjection(channelID int64, userIDs []int64) { + if r.channelFullProjectionCache == nil || channelID == 0 { + return + } + for _, userID := range userIDs { + if userID != 0 { + r.channelFullProjectionCache.DeletePair(userID, channelID) + } + } +} + func (r *Router) invalidateRPCProjectionForChannel(channelID int64) { if r.channelFullProjectionCache != nil { r.channelFullProjectionCache.DeleteChannel(channelID) diff --git a/internal/rpc/stickers_test.go b/internal/rpc/stickers_test.go index be66a173..495ad133 100644 --- a/internal/rpc/stickers_test.go +++ b/internal/rpc/stickers_test.go @@ -681,6 +681,64 @@ func TestTGDocumentDoesNotEmitAnimatedAttributeForTGSSticker(t *testing.T) { } } +func TestTGDocumentNeverEmitsZeroImageOrVideoDimensions(t *testing.T) { + // A sticker / custom emoji whose stored metadata carries a zero dimension + // must not reach the client as documentAttributeImageSize#0 -- Telegram + // Desktop divides by it and crashes when the emoji is used as a reaction. + sticker := tgDocument(domain.Document{ + ID: 100, AccessHash: 1, DCID: 2, MimeType: "application/x-tgsticker", + Attributes: []domain.DocumentAttribute{ + {Kind: domain.DocAttrImageSize, W: 0, H: 0}, + {Kind: domain.DocAttrCustomEmoji, Alt: "🙂", StickerSetID: 10, StickerSetAccessHash: 20}, + }, + }).(*tg.Document) + var gotImage bool + for _, attr := range sticker.Attributes { + if a, ok := attr.(*tg.DocumentAttributeImageSize); ok { + gotImage = true + if a.W != 512 || a.H != 512 { + t.Fatalf("sticker imageSize = %dx%d, want 512x512 fallback", a.W, a.H) + } + } + } + if !gotImage { + t.Fatal("sticker lost its imageSize attribute entirely") + } + + video := tgDocument(domain.Document{ + ID: 101, AccessHash: 1, DCID: 2, MimeType: "video/webm", + Attributes: []domain.DocumentAttribute{ + {Kind: domain.DocAttrVideo, W: 0, H: 0, Duration: 3}, + {Kind: domain.DocAttrSticker, Alt: "😀", StickerSetID: 10, StickerSetAccessHash: 20}, + }, + }).(*tg.Document) + for _, attr := range video.Attributes { + if a, ok := attr.(*tg.DocumentAttributeVideo); ok && (a.W <= 0 || a.H <= 0) { + t.Fatalf("video sticker attribute has zero dimension: %+v", a) + } + } + + // A malformed plain image drops the attribute rather than emitting a zero. + plain := tgDocument(domain.Document{ + ID: 102, AccessHash: 1, DCID: 2, MimeType: "image/jpeg", + Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrImageSize, W: 0, H: 480}}, + }).(*tg.Document) + for _, attr := range plain.Attributes { + if _, ok := attr.(*tg.DocumentAttributeImageSize); ok { + t.Fatalf("plain image kept a malformed imageSize: %+v", attr) + } + } + + // A zero-dimension thumbnail is dropped, not emitted. + thumbed := tgDocument(domain.Document{ + ID: 103, AccessHash: 1, DCID: 2, MimeType: "image/webp", + Thumbs: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "s", W: 0, H: 0, Size: 10}}, + }).(*tg.Document) + if len(thumbed.Thumbs) != 0 { + t.Fatalf("kept a zero-dimension thumb: %#v", thumbed.Thumbs) + } +} + func TestTGDocumentUsesDomainDocumentID(t *testing.T) { const documentID int64 = 1382305375846410902 diff --git a/internal/store/memory/channel_helpers.go b/internal/store/memory/channel_helpers.go index 41299c52..01a25b60 100644 --- a/internal/store/memory/channel_helpers.go +++ b/internal/store/memory/channel_helpers.go @@ -540,18 +540,14 @@ func (s *ChannelStore) resolveChannelReplyLocked(req domain.SendChannelMessageRe if req.ReplyTo.TopMessageID <= 0 || !channel.Forum { return nil, domain.ErrReplyMessageIDInvalid } - topic, ok := s.topics[req.ChannelID][req.ReplyTo.TopMessageID] - if !ok || topic.Hidden { - return nil, domain.ErrReplyMessageIDInvalid - } - if topic.Closed && !canManageForumTopic(channel, member, topic, req.UserID, selfBoostsApplied) { - return nil, domain.ErrChannelWriteForbidden - } reply := cloneMessageReply(req.ReplyTo) reply.MessageID = 0 reply.Peer = channelPeer - reply.TopMessageID = topic.TopicID reply.ForumTopic = true + if err := s.validateForumReplyTopicLocked(channel, member, req.ReplyTo.TopMessageID, req.UserID, selfBoostsApplied); err != nil { + return nil, err + } + reply.TopMessageID = req.ReplyTo.TopMessageID return reply, nil } target, ok := s.findMessageLocked(req.ChannelID, req.ReplyTo.MessageID) @@ -561,6 +557,22 @@ func (s *ChannelStore) resolveChannelReplyLocked(req domain.SendChannelMessageRe reply := cloneMessageReply(req.ReplyTo) reply.MessageID = target.ID reply.Peer = channelPeer + + if channel.Forum { + // A forum reply belongs to the TARGET's topic, never the target's own id. + topicID := domain.ForumReplyTopicID(target) + if req.ReplyTo.TopMessageID > 0 && req.ReplyTo.TopMessageID != topicID { + return nil, domain.ErrReplyMessageIDInvalid + } + if err := s.validateForumReplyTopicLocked(channel, member, topicID, req.UserID, selfBoostsApplied); err != nil { + return nil, err + } + reply.TopMessageID = topicID + reply.ForumTopic = true + return reply, nil + } + + // Non-forum discussion thread: reply_to_top_id is the comment-thread root. reply.TopMessageID = target.ID if target.ReplyTo != nil && target.ReplyTo.TopMessageID > 0 { reply.TopMessageID = target.ReplyTo.TopMessageID @@ -568,17 +580,25 @@ func (s *ChannelStore) resolveChannelReplyLocked(req domain.SendChannelMessageRe if req.ReplyTo.TopMessageID > 0 && req.ReplyTo.TopMessageID != reply.TopMessageID { return nil, domain.ErrReplyMessageIDInvalid } - if channel.Forum && reply.TopMessageID > 0 { - if topic, ok := s.topics[req.ChannelID][reply.TopMessageID]; ok && !topic.Hidden { - if topic.Closed && !canManageForumTopic(channel, member, topic, req.UserID, selfBoostsApplied) { - return nil, domain.ErrChannelWriteForbidden - } - reply.ForumTopic = true - } - } return reply, nil } +// validateForumReplyTopicLocked mirrors the postgres store: General +// (ForumGeneralTopicID) is a virtual topic with no row and is always valid. +func (s *ChannelStore) validateForumReplyTopicLocked(channel domain.Channel, member domain.ChannelMember, topicID int, userID int64, selfBoostsApplied int) error { + if topicID == domain.ForumGeneralTopicID { + return nil + } + topic, ok := s.topics[channel.ID][topicID] + if !ok || topic.Hidden { + return domain.ErrReplyMessageIDInvalid + } + if topic.Closed && !canManageForumTopic(channel, member, topic, userID, selfBoostsApplied) { + return domain.ErrChannelWriteForbidden + } + return nil +} + func inactiveChannelDate(dialog domain.Dialog, channel domain.Channel, member domain.ChannelMember) int { if dialog.TopMessageDate > 0 { return dialog.TopMessageDate diff --git a/internal/store/memory/channel_settings.go b/internal/store/memory/channel_settings.go index 08134f56..c5c173cc 100644 --- a/internal/store/memory/channel_settings.go +++ b/internal/store/memory/channel_settings.go @@ -128,6 +128,9 @@ func (s *ChannelStore) CheckUsername(_ context.Context, userID, channelID int64, return false, err } usernameLower := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(username, "@"))) + if s.usernameRegistry != nil && s.usernameRegistry.nameReserved(usernameLower) { + return false, nil + } for id, channel := range s.channels { if channel.Deleted || channel.Username == "" { continue @@ -175,6 +178,13 @@ func (s *ChannelStore) UpdateUsername(ctx context.Context, req domain.UpdateChan } prevUsername := channel.Username channel.Username = username + // A public group cannot keep pre-history hidden: assigning a username forces + // "chat history for new members" back to visible (matches the official + // server). Removing the username leaves the flag untouched. + clearedPrehistory := username != "" && channel.PreHistoryHidden + if clearedPrehistory { + channel.PreHistoryHidden = false + } s.channels[req.ChannelID] = channel s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{ ChannelID: req.ChannelID, @@ -184,6 +194,16 @@ func (s *ChannelStore) UpdateUsername(ctx context.Context, req domain.UpdateChan PrevString: prevUsername, NewString: username, }) + if clearedPrehistory { + s.appendChannelAdminLogLocked(domain.ChannelAdminLogEvent{ + ChannelID: req.ChannelID, + UserID: req.UserID, + Date: int(time.Now().Unix()), + Type: domain.ChannelAdminLogTogglePreHistoryHidden, + PrevBool: true, + NewBool: false, + }) + } return channel, nil } diff --git a/internal/store/memory/channel_topic_read.go b/internal/store/memory/channel_topic_read.go index f522324d..5007c74b 100644 --- a/internal/store/memory/channel_topic_read.go +++ b/internal/store/memory/channel_topic_read.go @@ -178,7 +178,7 @@ func (s *ChannelStore) GeneralForumTopic(_ context.Context, viewerUserID, channe } s.mu.RLock() defer s.mu.RUnlock() - channel, member, err := s.channelAndMemberLocked(viewerUserID, channelID) + channel, member, _, err := s.channelForViewerLocked(viewerUserID, channelID) if err != nil { return domain.ChannelForumTopic{}, err } diff --git a/internal/store/memory/channel_topics.go b/internal/store/memory/channel_topics.go index 99bd8eef..173c4a37 100644 --- a/internal/store/memory/channel_topics.go +++ b/internal/store/memory/channel_topics.go @@ -420,7 +420,9 @@ func (s *ChannelStore) DeleteForumTopicHistory(_ context.Context, req domain.Del func (s *ChannelStore) ListForumTopics(_ context.Context, viewerUserID int64, filter domain.ChannelForumTopicFilter) (domain.ChannelForumTopicList, error) { s.mu.RLock() defer s.mu.RUnlock() - channel, member, err := s.channelAndMemberLocked(viewerUserID, filter.ChannelID) + // channelForViewerLocked, not channelAndMemberLocked: a public forum's topic + // list is browsable before joining, exactly like its message history. + channel, member, _, err := s.channelForViewerLocked(viewerUserID, filter.ChannelID) if err != nil { return domain.ChannelForumTopicList{}, err } @@ -463,7 +465,7 @@ func (s *ChannelStore) ListForumTopics(_ context.Context, viewerUserID int64, fi func (s *ChannelStore) GetForumTopicsByID(_ context.Context, viewerUserID, channelID int64, ids []int) (domain.ChannelForumTopicList, error) { s.mu.RLock() defer s.mu.RUnlock() - channel, member, err := s.channelAndMemberLocked(viewerUserID, channelID) + channel, member, _, err := s.channelForViewerLocked(viewerUserID, channelID) if err != nil { return domain.ChannelForumTopicList{}, err } @@ -499,7 +501,9 @@ func (s *ChannelStore) GetForumTopicsByID(_ context.Context, viewerUserID, chann func (s *ChannelStore) ListChannelReplies(_ context.Context, viewerUserID int64, filter domain.ChannelRepliesFilter) (domain.ChannelHistory, error) { s.mu.RLock() defer s.mu.RUnlock() - source, member, err := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, filter.ChannelID) + // Viewer scope (not strict membership): non-members can preview topic + // replies in a public channel/supergroup, matching ListChannelHistory. + source, member, _, err := s.channelForViewerLocked(viewerUserID, filter.ChannelID) if err != nil { return domain.ChannelHistory{}, err } diff --git a/internal/store/memory/collectible_username.go b/internal/store/memory/collectible_username.go index 6f0052c8..f4c79f8a 100644 --- a/internal/store/memory/collectible_username.go +++ b/internal/store/memory/collectible_username.go @@ -55,6 +55,26 @@ type CollectibleUsernameStore struct { transfers map[int64][]domain.CollectibleUsernameTransfer // commands maps a provenance command key onto the asset it touched. commands map[string]int64 + // reserved, when set, is the operator blocklist consulted before a name is + // assigned to an editable slot or minted, mirroring the PostgreSQL checks. + reserved *ReservedUsernameStore +} + +// WithReservedUsernames wires the operator blocklist into the registry so a +// reserved name is refused, matching PostgreSQL. +func (s *CollectibleUsernameStore) WithReservedUsernames(reserved *ReservedUsernameStore) *CollectibleUsernameStore { + s.reserved = reserved + return s +} + +// nameReserved reports whether a name is on the operator blocklist. It touches +// only s.reserved (its own lock), so it is safe from any context. +func (s *CollectibleUsernameStore) nameReserved(usernameLower string) bool { + if s == nil || s.reserved == nil { + return false + } + r, _ := s.reserved.IsReserved(context.Background(), usernameLower) + return r } // collectibleRegistryRow is one peer_usernames row: the owning peer plus the @@ -101,6 +121,9 @@ func (s *CollectibleUsernameStore) SetEditableUsername(_ context.Context, peer d return false, domain.ErrUsernameInvalid } key := strings.ToLower(username) + if s.nameReserved(key) { + return false, domain.ErrUsernameOccupied + } if existing, ok := s.registry[key]; ok { if existing.peer == peer && existing.row.Editable { if existing.row.Username == username { @@ -313,6 +336,9 @@ func (s *CollectibleUsernameStore) MintCollectibleUsername(_ context.Context, re if _, ok := s.registry[key]; ok { return domain.CollectibleUsername{}, false, domain.ErrUsernameOccupied } + if s.nameReserved(key) { + return domain.CollectibleUsername{}, false, domain.ErrUsernameOccupied + } now := time.Now().UTC() purchaseDate := req.PurchaseDate if purchaseDate.IsZero() { diff --git a/internal/store/memory/reserved_username.go b/internal/store/memory/reserved_username.go new file mode 100644 index 00000000..9da2e108 --- /dev/null +++ b/internal/store/memory/reserved_username.go @@ -0,0 +1,100 @@ +package memory + +import ( + "context" + "sort" + "strings" + "sync" + "time" + + "telesrv/internal/domain" +) + +// ReservedUsernameStore is the in-memory operator username blocklist. +type ReservedUsernameStore struct { + mu sync.Mutex + entries map[string]domain.ReservedUsername // keyed by username_lower +} + +// NewReservedUsernameStore creates an empty blocklist. +func NewReservedUsernameStore() *ReservedUsernameStore { + return &ReservedUsernameStore{entries: make(map[string]domain.ReservedUsername)} +} + +func (s *ReservedUsernameStore) IsReserved(_ context.Context, usernameLower string) (bool, error) { + if s == nil { + return false, nil + } + usernameLower = strings.ToLower(strings.TrimSpace(usernameLower)) + if usernameLower == "" { + return false, nil + } + s.mu.Lock() + defer s.mu.Unlock() + _, ok := s.entries[usernameLower] + return ok, nil +} + +func (s *ReservedUsernameStore) ReserveUsername(_ context.Context, username, reason, actor string) (bool, error) { + username = strings.TrimSpace(username) + lower := strings.ToLower(username) + if lower == "" { + return false, domain.ErrUsernameInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.entries[lower]; ok { + return false, nil + } + s.entries[lower] = domain.ReservedUsername{Username: username, Reason: reason, Actor: actor, CreatedAt: time.Now().UTC()} + return true, nil +} + +func (s *ReservedUsernameStore) UnreserveUsername(_ context.Context, username string) (bool, error) { + lower := strings.ToLower(strings.TrimSpace(username)) + if lower == "" { + return false, domain.ErrUsernameInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.entries[lower]; !ok { + return false, nil + } + delete(s.entries, lower) + return true, nil +} + +func (s *ReservedUsernameStore) ReservedUsernames(_ context.Context, filter domain.ReservedUsernameFilter) ([]domain.ReservedUsername, error) { + s.mu.Lock() + defer s.mu.Unlock() + q := strings.ToLower(strings.TrimSpace(filter.Query)) + out := make([]domain.ReservedUsername, 0, len(s.entries)) + for key, entry := range s.entries { + if q != "" && !strings.HasPrefix(key, q) { + continue + } + out = append(out, entry) + } + sort.Slice(out, func(i, j int) bool { + if !out[i].CreatedAt.Equal(out[j].CreatedAt) { + return out[i].CreatedAt.After(out[j].CreatedAt) + } + return strings.ToLower(out[i].Username) < strings.ToLower(out[j].Username) + }) + limit := filter.Limit + if limit <= 0 || limit > 500 { + limit = 100 + } + offset := filter.Offset + if offset < 0 { + offset = 0 + } + if offset >= len(out) { + return []domain.ReservedUsername{}, nil + } + end := offset + limit + if end > len(out) { + end = len(out) + } + return out[offset:end], nil +} diff --git a/internal/store/memory/reserved_username_test.go b/internal/store/memory/reserved_username_test.go new file mode 100644 index 00000000..f6f01e9b --- /dev/null +++ b/internal/store/memory/reserved_username_test.go @@ -0,0 +1,37 @@ +package memory + +import ( + "context" + "testing" + + "telesrv/internal/domain" +) + +func TestCheckUsernameReportsReservedAsTaken(t *testing.T) { + ctx := context.Background() + reserved := NewReservedUsernameStore() + if _, err := reserved.ReserveUsername(ctx, "support", "official", "ops"); err != nil { + t.Fatalf("seed reserve: %v", err) + } + registry := NewCollectibleUsernameStore().WithReservedUsernames(reserved) + + users := NewUserStore() + users.AttachUsernameRegistry(registry) + u, _ := users.Create(ctx, domain.User{AccessHash: 1, Phone: "15550001000", FirstName: "A"}) + if ok, err := users.CheckUsername(ctx, u.ID, "support"); err != nil || ok { + t.Fatalf("CheckUsername(reserved) = %v, %v; want false, nil", ok, err) + } + if ok, err := users.CheckUsername(ctx, u.ID, "freename"); err != nil || !ok { + t.Fatalf("CheckUsername(free) = %v, %v; want true, nil", ok, err) + } + + channels := NewChannelStore() + channels.AttachUsernameRegistry(registry) + created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{CreatorUserID: u.ID, Title: "C", Megagroup: true, Date: 1}) + if err != nil { + t.Fatalf("create channel: %v", err) + } + if ok, err := channels.CheckUsername(ctx, u.ID, created.Channel.ID, "support"); err != nil || ok { + t.Fatalf("channel CheckUsername(reserved) = %v, %v; want false, nil", ok, err) + } +} diff --git a/internal/store/memory/users.go b/internal/store/memory/users.go index 196851a7..0b3656c0 100644 --- a/internal/store/memory/users.go +++ b/internal/store/memory/users.go @@ -158,6 +158,9 @@ func (s *UserStore) CheckUsername(_ context.Context, userID int64, username stri if username == "" { return true, nil } + if s.usernameRegistry != nil && s.usernameRegistry.nameReserved(username) { + return false, nil + } s.mu.RLock() defer s.mu.RUnlock() for id, u := range s.byID { diff --git a/internal/store/postgres/channel_helpers.go b/internal/store/postgres/channel_helpers.go index 6edb8d60..ae2647dd 100644 --- a/internal/store/postgres/channel_helpers.go +++ b/internal/store/postgres/channel_helpers.go @@ -795,21 +795,14 @@ func (s *ChannelStore) resolveChannelReply(ctx context.Context, db sqlcgen.DBTX, if req.ReplyTo.TopMessageID <= 0 || !channel.Forum { return nil, domain.ErrReplyMessageIDInvalid } - topic, err := s.getForumTopic(ctx, db, req.ChannelID, req.ReplyTo.TopMessageID) - if err != nil { - return nil, domain.ErrReplyMessageIDInvalid - } - if topic.Hidden { - return nil, domain.ErrReplyMessageIDInvalid - } - if topic.Closed && !canManageForumTopic(channel, member, topic, req.UserID, selfBoostsApplied) { - return nil, domain.ErrChannelWriteForbidden - } reply := cloneMessageReply(req.ReplyTo) reply.MessageID = 0 reply.Peer = channelPeer - reply.TopMessageID = topic.TopicID reply.ForumTopic = true + if err := s.validateForumReplyTopic(ctx, db, channel, member, req.ReplyTo.TopMessageID, req.UserID, selfBoostsApplied); err != nil { + return nil, err + } + reply.TopMessageID = req.ReplyTo.TopMessageID return reply, nil } target, err := s.getChannelMessage(ctx, db, req.ChannelID, req.ReplyTo.MessageID) @@ -825,6 +818,22 @@ func (s *ChannelStore) resolveChannelReply(ctx context.Context, db sqlcgen.DBTX, reply := cloneMessageReply(req.ReplyTo) reply.MessageID = target.ID reply.Peer = channelPeer + + if channel.Forum { + // A forum reply belongs to the TARGET's topic, never the target's own id. + topicID := domain.ForumReplyTopicID(target) + if req.ReplyTo.TopMessageID > 0 && req.ReplyTo.TopMessageID != topicID { + return nil, domain.ErrReplyMessageIDInvalid + } + if err := s.validateForumReplyTopic(ctx, db, channel, member, topicID, req.UserID, selfBoostsApplied); err != nil { + return nil, err + } + reply.TopMessageID = topicID + reply.ForumTopic = true + return reply, nil + } + + // Non-forum discussion thread: reply_to_top_id is the comment-thread root. reply.TopMessageID = target.ID if target.ReplyTo != nil && target.ReplyTo.TopMessageID > 0 { reply.TopMessageID = target.ReplyTo.TopMessageID @@ -832,19 +841,29 @@ func (s *ChannelStore) resolveChannelReply(ctx context.Context, db sqlcgen.DBTX, if req.ReplyTo.TopMessageID > 0 && req.ReplyTo.TopMessageID != reply.TopMessageID { return nil, domain.ErrReplyMessageIDInvalid } - if channel.Forum && reply.TopMessageID > 0 { - if topic, err := s.getForumTopic(ctx, db, req.ChannelID, reply.TopMessageID); err == nil && !topic.Hidden { - if topic.Closed && !canManageForumTopic(channel, member, topic, req.UserID, selfBoostsApplied) { - return nil, domain.ErrChannelWriteForbidden - } - reply.ForumTopic = true - } else if err != nil && !errors.Is(err, domain.ErrMessageIDInvalid) { - return nil, err - } - } return reply, nil } +// validateForumReplyTopic checks that topicID is a topic the caller may post +// into. General (ForumGeneralTopicID) is a virtual topic with no +// channel_forum_topics row and is always valid. +func (s *ChannelStore) validateForumReplyTopic(ctx context.Context, db sqlcgen.DBTX, channel domain.Channel, member domain.ChannelMember, topicID int, userID int64, selfBoostsApplied int) error { + if topicID == domain.ForumGeneralTopicID { + return nil + } + topic, err := s.getForumTopic(ctx, db, channel.ID, topicID) + if err != nil { + return domain.ErrReplyMessageIDInvalid + } + if topic.Hidden { + return domain.ErrReplyMessageIDInvalid + } + if topic.Closed && !canManageForumTopic(channel, member, topic, userID, selfBoostsApplied) { + return domain.ErrChannelWriteForbidden + } + return nil +} + func visibleChannelTopAfter(ctx context.Context, db sqlcgen.DBTX, channelID int64, availableMinID int, fallbackDate int) (int, int, error) { var id, date int err := db.QueryRow(ctx, ` diff --git a/internal/store/postgres/channel_invite_import.go b/internal/store/postgres/channel_invite_import.go index 8cf5ceb9..50ce7401 100644 --- a/internal/store/postgres/channel_invite_import.go +++ b/internal/store/postgres/channel_invite_import.go @@ -103,6 +103,7 @@ func (s *ChannelStore) ImportInvite(ctx context.Context, req domain.ImportChanne return domain.CreateChannelResult{}, fmt.Errorf("commit import channel invite: %w", err) } committed = true + s.invalidateChannelMembershipCaches(result.Channel.ID, req.UserID) recipients, _ := s.ListActiveChannelMemberIDs(ctx, req.UserID, result.Channel.ID, 0) result.Recipients = recipients return result, nil diff --git a/internal/store/postgres/channel_invite_members.go b/internal/store/postgres/channel_invite_members.go index ee546aaf..7a2e582c 100644 --- a/internal/store/postgres/channel_invite_members.go +++ b/internal/store/postgres/channel_invite_members.go @@ -125,6 +125,7 @@ func (s *ChannelStore) InviteToChannel(ctx context.Context, channelID, inviterUs return domain.CreateChannelResult{}, fmt.Errorf("commit invite channel: %w", err) } committed = true + s.invalidateChannelMembershipCaches(channelID, invitedIDs...) recipients, _ := s.ListActiveChannelMemberIDs(ctx, inviterUserID, channelID, 0) return domain.CreateChannelResult{Channel: channel, Members: members, Message: msg, Event: event, Recipients: recipients}, nil } diff --git a/internal/store/postgres/channel_member_join.go b/internal/store/postgres/channel_member_join.go index 5a77bd45..aa188c11 100644 --- a/internal/store/postgres/channel_member_join.go +++ b/internal/store/postgres/channel_member_join.go @@ -130,6 +130,7 @@ WHERE channel_id = $1 AND user_id = $2`, channelID, userID, member.ReadInboxMaxI return domain.CreateChannelResult{}, fmt.Errorf("commit join channel: %w", err) } committed = true + s.invalidateChannelMembershipCaches(channelID, userID) recipients, _ := s.ListActiveChannelMemberIDs(ctx, userID, channelID, 0) return domain.CreateChannelResult{Channel: channel, Members: []domain.ChannelMember{member}, Message: msg, Event: event, Recipients: recipients}, nil } @@ -259,6 +260,11 @@ WHERE id = $1`, channelID, channel.CreatorUserID, adminsDelta); err != nil { return domain.CreateChannelResult{}, fmt.Errorf("commit leave channel: %w", err) } committed = true + leftUserIDs := make([]int64, 0, len(members)) + for _, m := range members { + leftUserIDs = append(leftUserIDs, m.UserID) + } + s.invalidateChannelMembershipCaches(channelID, leftUserIDs...) recipients = append(recipients, userID) return domain.CreateChannelResult{Channel: channel, Members: members, Message: msg, Event: event, Recipients: recipients}, nil } diff --git a/internal/store/postgres/channel_settings.go b/internal/store/postgres/channel_settings.go index e2082476..f9c8625b 100644 --- a/internal/store/postgres/channel_settings.go +++ b/internal/store/postgres/channel_settings.go @@ -241,12 +241,29 @@ func (s *ChannelStore) UpdateUsername(ctx context.Context, req domain.UpdateChan if err := replacePeerUsernameTx(ctx, tx, peerUsernameTypeChannel, req.ChannelID, username, usernameLower); err != nil { return domain.Channel{}, err } - if _, err := tx.Exec(ctx, `UPDATE channels SET username = NULLIF($2,''), updated_at = now() WHERE id = $1`, req.ChannelID, username); err != nil { + // A public group cannot keep pre-history hidden: assigning a username forces + // "chat history for new members" back to visible (matches the official + // server). Removing the username leaves the flag untouched, so the creator + // can hide history again once the group is private. + if _, err := tx.Exec(ctx, `UPDATE channels SET username = NULLIF($2,''), pre_history_hidden = (pre_history_hidden AND $2 = ''), updated_at = now() WHERE id = $1`, req.ChannelID, username); err != nil { return domain.Channel{}, fmt.Errorf("update channel username: %w", err) } if err := markUserChannelMemberIndexPublicTx(ctx, tx, req.ChannelID, username != ""); err != nil { return domain.Channel{}, err } + if username != "" && channel.PreHistoryHidden { + if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{ + ChannelID: req.ChannelID, + UserID: req.UserID, + Date: nowUnix(), + Type: domain.ChannelAdminLogTogglePreHistoryHidden, + PrevBool: true, + NewBool: false, + }); err != nil { + return domain.Channel{}, err + } + channel.PreHistoryHidden = false + } prevUsername := channel.Username channel.Username = username if err := s.insertChannelAdminLogTx(ctx, tx, domain.ChannelAdminLogEvent{ diff --git a/internal/store/postgres/channel_store.go b/internal/store/postgres/channel_store.go index b7c3e329..54de6c3d 100644 --- a/internal/store/postgres/channel_store.go +++ b/internal/store/postgres/channel_store.go @@ -95,6 +95,33 @@ func (s *ChannelStore) boostCacheActive(db sqlcgen.DBTX) bool { return s.boostCache != nil && db == s.db } +// invalidateChannelMembershipCaches drops the in-process reads that a membership +// change (join, leave, invite, kick) makes stale for the given users. The +// ReadModelChangeListener also clears these off the async NOTIFY, but callers +// must not depend on that round-trip: a client that polls channels.getFullChannel +// right after channels.leaveChannel would otherwise keep seeing itself as an +// active member (and keep an open compose box) until the notify lands. Call it +// post-commit. +func (s *ChannelStore) invalidateChannelMembershipCaches(channelID int64, userIDs ...int64) { + if channelID == 0 { + return + } + if s.rowCache != nil { + s.rowCache.delete(channelID) + } + for _, userID := range userIDs { + if userID == 0 { + continue + } + if s.memberCache != nil { + s.memberCache.delete(channelID, userID) + } + if s.dialogCache != nil { + s.dialogCache.delete(userID, channelID) + } + } +} + // NewChannelStore 基于 pgx 连接池(或事务)创建 ChannelStore。 func NewChannelStore(db sqlcgen.DBTX, opts ...ChannelStoreOption) *ChannelStore { s := &ChannelStore{db: db} diff --git a/internal/store/postgres/channel_topic_read.go b/internal/store/postgres/channel_topic_read.go index f65a031c..b1d558fa 100644 --- a/internal/store/postgres/channel_topic_read.go +++ b/internal/store/postgres/channel_topic_read.go @@ -198,7 +198,7 @@ func (s *ChannelStore) GeneralForumTopic(ctx context.Context, viewerUserID, chan if viewerUserID == 0 || channelID == 0 { return domain.ChannelForumTopic{}, domain.ErrChannelInvalid } - channel, member, err := s.getChannelForMember(ctx, s.db, viewerUserID, channelID) + channel, member, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID) if err != nil { return domain.ChannelForumTopic{}, err } diff --git a/internal/store/postgres/channel_topics.go b/internal/store/postgres/channel_topics.go index 53426b81..a3b3fc7c 100644 --- a/internal/store/postgres/channel_topics.go +++ b/internal/store/postgres/channel_topics.go @@ -470,7 +470,9 @@ WHERE channel_id = $1 AND topic_id = $2`, req.ChannelID, req.TopicID); err != ni } func (s *ChannelStore) ListForumTopics(ctx context.Context, viewerUserID int64, filter domain.ChannelForumTopicFilter) (domain.ChannelForumTopicList, error) { - channel, member, err := s.getChannelForMember(ctx, s.db, viewerUserID, filter.ChannelID) + // getChannelForViewer, not getChannelForMember: a public forum's topic list is + // browsable before joining, exactly like its message history. + channel, member, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, filter.ChannelID) if err != nil { return domain.ChannelForumTopicList{}, err } @@ -539,7 +541,7 @@ LIMIT $`+fmt.Sprint(len(args)), args...) } func (s *ChannelStore) GetForumTopicsByID(ctx context.Context, viewerUserID, channelID int64, ids []int) (domain.ChannelForumTopicList, error) { - channel, member, err := s.getChannelForMember(ctx, s.db, viewerUserID, channelID) + channel, member, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID) if err != nil { return domain.ChannelForumTopicList{}, err } @@ -586,7 +588,9 @@ ORDER BY pinned DESC, pinned_order DESC, date DESC, topic_id DESC`, channelID, m } func (s *ChannelStore) ListChannelReplies(ctx context.Context, viewerUserID int64, filter domain.ChannelRepliesFilter) (domain.ChannelHistory, error) { - source, member, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, filter.ChannelID) + // Viewer口径(非严格 member):公开频道/超级群的非成员可预览话题回复,与 + // ListChannelHistory 一致。私有频道非成员仍是 ErrChannelPrivate。 + source, member, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, filter.ChannelID) if err != nil { return domain.ChannelHistory{}, err } diff --git a/internal/store/postgres/collectible_username.go b/internal/store/postgres/collectible_username.go index db26c0d2..c5547074 100644 --- a/internal/store/postgres/collectible_username.go +++ b/internal/store/postgres/collectible_username.go @@ -206,6 +206,11 @@ func (s *CollectibleUsernameStore) MintCollectibleUsername(ctx context.Context, } else if found { return domain.ErrUsernameOccupied } + if reserved, err := usernameReservedTx(ctx, tx, usernameLower); err != nil { + return err + } else if reserved { + return domain.ErrUsernameOccupied + } var existing int64 switch err := tx.QueryRow(ctx, ` SELECT id FROM collectible_usernames diff --git a/internal/store/postgres/peer_username.go b/internal/store/postgres/peer_username.go index 5a00a838..7af1face 100644 --- a/internal/store/postgres/peer_username.go +++ b/internal/store/postgres/peer_username.go @@ -61,7 +61,27 @@ func getPeerUsernameOwner(ctx context.Context, db sqlcgen.DBTX, usernameLower st return owner, true, nil } +// usernameReservedTx reports whether a name is on the operator blocklist. It is +// consulted before every editable-username write and before a collectible mint. +func usernameReservedTx(ctx context.Context, db sqlcgen.DBTX, usernameLower string) (bool, error) { + if usernameLower == "" { + return false, nil + } + var exists bool + if err := db.QueryRow(ctx, + `SELECT EXISTS (SELECT 1 FROM reserved_usernames WHERE username_lower = $1)`, + usernameLower).Scan(&exists); err != nil { + return false, fmt.Errorf("check reserved username: %w", err) + } + return exists, nil +} + func peerUsernameAvailable(ctx context.Context, db sqlcgen.DBTX, usernameLower, peerType string, peerID int64) (bool, error) { + if reserved, err := usernameReservedTx(ctx, db, usernameLower); err != nil { + return false, err + } else if reserved { + return false, nil + } owner, found, err := getPeerUsernameOwner(ctx, db, usernameLower, false) if err != nil || !found { return !found, err @@ -111,6 +131,11 @@ WHERE peer_type = $1 // otherwise account.updateUsername would silently release a minted asset. func replacePeerUsernameTx(ctx context.Context, tx pgx.Tx, peerType string, peerID int64, username, usernameLower string) error { if usernameLower != "" { + if reserved, err := usernameReservedTx(ctx, tx, usernameLower); err != nil { + return err + } else if reserved { + return domain.ErrUsernameOccupied + } owner, found, err := getPeerUsernameOwner(ctx, tx, usernameLower, true) if err != nil { return err diff --git a/internal/store/postgres/reserved_username.go b/internal/store/postgres/reserved_username.go new file mode 100644 index 00000000..37782bfe --- /dev/null +++ b/internal/store/postgres/reserved_username.go @@ -0,0 +1,99 @@ +package postgres + +import ( + "context" + "fmt" + "strings" + + "telesrv/internal/domain" + "telesrv/internal/store/postgres/sqlcgen" +) + +// ReservedUsernameStore is the operator username blocklist backed by the +// reserved_usernames table. +type ReservedUsernameStore struct { + db sqlcgen.DBTX +} + +// NewReservedUsernameStore builds the store on a pgx pool or transaction. +func NewReservedUsernameStore(db sqlcgen.DBTX) *ReservedUsernameStore { + return &ReservedUsernameStore{db: db} +} + +func (s *ReservedUsernameStore) IsReserved(ctx context.Context, usernameLower string) (bool, error) { + usernameLower = strings.ToLower(strings.TrimSpace(usernameLower)) + if usernameLower == "" { + return false, nil + } + var exists bool + if err := s.db.QueryRow(ctx, + `SELECT EXISTS (SELECT 1 FROM reserved_usernames WHERE username_lower = $1)`, + usernameLower).Scan(&exists); err != nil { + return false, fmt.Errorf("check reserved username: %w", err) + } + return exists, nil +} + +func (s *ReservedUsernameStore) ReserveUsername(ctx context.Context, username, reason, actor string) (bool, error) { + username = strings.TrimSpace(username) + lower := strings.ToLower(username) + if lower == "" { + return false, domain.ErrUsernameInvalid + } + tag, err := s.db.Exec(ctx, ` +INSERT INTO reserved_usernames (username_lower, username, reason, actor) +VALUES ($1, $2, $3, $4) +ON CONFLICT (username_lower) DO NOTHING`, lower, username, reason, actor) + if err != nil { + return false, fmt.Errorf("reserve username: %w", err) + } + return tag.RowsAffected() > 0, nil +} + +func (s *ReservedUsernameStore) UnreserveUsername(ctx context.Context, username string) (bool, error) { + lower := strings.ToLower(strings.TrimSpace(username)) + if lower == "" { + return false, domain.ErrUsernameInvalid + } + tag, err := s.db.Exec(ctx, `DELETE FROM reserved_usernames WHERE username_lower = $1`, lower) + if err != nil { + return false, fmt.Errorf("unreserve username: %w", err) + } + return tag.RowsAffected() > 0, nil +} + +func (s *ReservedUsernameStore) ReservedUsernames(ctx context.Context, filter domain.ReservedUsernameFilter) ([]domain.ReservedUsername, error) { + limit := filter.Limit + if limit <= 0 || limit > 500 { + limit = 100 + } + offset := filter.Offset + if offset < 0 { + offset = 0 + } + args := []any{limit, offset} + where := "" + if q := strings.ToLower(strings.TrimSpace(filter.Query)); q != "" { + args = append(args, q+"%") + where = "WHERE username_lower LIKE $3" + } + rows, err := s.db.Query(ctx, ` +SELECT username, reason, actor, created_at +FROM reserved_usernames +`+where+` +ORDER BY created_at DESC, username_lower +LIMIT $1 OFFSET $2`, args...) + if err != nil { + return nil, fmt.Errorf("list reserved usernames: %w", err) + } + defer rows.Close() + out := make([]domain.ReservedUsername, 0, limit) + for rows.Next() { + var item domain.ReservedUsername + if err := rows.Scan(&item.Username, &item.Reason, &item.Actor, &item.CreatedAt); err != nil { + return nil, fmt.Errorf("scan reserved username: %w", err) + } + out = append(out, item) + } + return out, rows.Err() +} diff --git a/internal/store/reserved_username.go b/internal/store/reserved_username.go new file mode 100644 index 00000000..8da8c2a9 --- /dev/null +++ b/internal/store/reserved_username.go @@ -0,0 +1,22 @@ +package store + +import ( + "context" + + "telesrv/internal/domain" +) + +// ReservedUsernameStore owns the operator username blocklist. IsReserved is the +// hot path consulted on every editable-username write; the rest are the admin +// lifecycle. +type ReservedUsernameStore interface { + // IsReserved reports whether usernameLower (already lowercased) is blocked. + IsReserved(ctx context.Context, usernameLower string) (bool, error) + // ReserveUsername adds an entry. Returns created=false if it already existed + // (the existing reason/actor are kept). + ReserveUsername(ctx context.Context, username, reason, actor string) (created bool, err error) + // UnreserveUsername removes an entry. Returns removed=false if absent. + UnreserveUsername(ctx context.Context, username string) (removed bool, err error) + // ReservedUsernames pages the blocklist, newest first. + ReservedUsernames(ctx context.Context, filter domain.ReservedUsernameFilter) ([]domain.ReservedUsername, error) +} diff --git a/internal/webauthn/webauthn.go b/internal/webauthn/webauthn.go index 7c1bfd99..82d44bd5 100644 --- a/internal/webauthn/webauthn.go +++ b/internal/webauthn/webauthn.go @@ -101,7 +101,7 @@ func BuildRegistrationOptions(p RegistrationParams) ([]byte, error) { exclude = append(exclude, map[string]any{"type": "public-key", "id": b64.EncodeToString(id)}) } pub := map[string]any{ - "rp": map[string]any{"id": p.RPID, "name": orDefault(p.RPName, branding.ProductName)}, + "rp": map[string]any{"id": p.RPID, "name": orDefault(p.RPName, branding.ProductName())}, "user": map[string]any{"id": b64.EncodeToString(p.UserID), "name": p.UserName, "displayName": orDefault(p.UserDisplay, p.UserName)}, "challenge": b64.EncodeToString(p.Challenge), "pubKeyCredParams": []map[string]any{