From fff8de783aed0cb82a29f06a42f2442c7378b005 Mon Sep 17 00:00:00 2001 From: Egor Egorov <73982770+epilepticseizureee@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:18:00 +0300 Subject: [PATCH] feat: add NFT usernames and bot verification (#22) Implements collectible usernames, official verification workflows, and third-party bot verification after maintainer protocol and migration review. The composite activity/moderation rating remains an admin-only read model; Telegram Stars Rating wire fields stay unset pending a dedicated official-semantics implementation. Reviewed-Head: 2796345775ea0f908fb7734601e5e1dee4b653b9 Original-Head: fa082b892fd5180c9c9bc53c81c21cf5d250a75b Co-authored-by: Egor Egorov --- .env.example | 99 + cmd/telesrv-admin/botverification.go | 569 ++++++ cmd/telesrv-admin/botverification_test.go | 559 ++++++ cmd/telesrv-admin/main.go | 6 + cmd/telesrv-admin/readstore.go | 1387 +++++++++++++- .../readstore_accounts_integration_test.go | 146 ++ ...dstore_botverification_integration_test.go | 571 ++++++ ...readstore_verification_integration_test.go | 354 ++++ cmd/telesrv-admin/security.go | 210 +++ cmd/telesrv-admin/server.go | 514 +++++- cmd/telesrv-admin/session.go | 50 + cmd/telesrv-admin/session_test.go | 211 +++ cmd/telesrv-admin/verification.go | 262 +++ cmd/telesrv-admin/verification_test.go | 690 +++++++ .../web/dist/assets/index-D5Lc7N2D.css | 1 - .../web/dist/assets/index-DJw3UpEg.js | 9 - .../web/dist/assets/index-D_BLAfeq.js | 9 + .../web/dist/assets/index-KZOn7Xwd.css | 1 + cmd/telesrv-admin/web/dist/index.html | 4 +- cmd/telesrv-admin/web/package-lock.json | 14 +- cmd/telesrv-admin/web/src/App.tsx | 34 +- cmd/telesrv-admin/web/src/api.ts | 135 +- .../web/src/components/ActionButton.tsx | 15 +- .../web/src/components/EntityPicker.tsx | 99 +- .../web/src/components/Layout.tsx | 19 + cmd/telesrv-admin/web/src/components/ui.tsx | 34 +- cmd/telesrv-admin/web/src/i18n.tsx | 1128 +++++++++++- cmd/telesrv-admin/web/src/lib/format.ts | 119 ++ .../web/src/pages/AccountDetailPage.tsx | 7 +- .../web/src/pages/AccountRatingDetailPage.tsx | 261 +++ .../web/src/pages/AccountRatingsPage.tsx | 167 ++ .../web/src/pages/AccountsPage.tsx | 6 +- .../web/src/pages/BotVerificationPage.tsx | 965 ++++++++++ .../src/pages/BotVerificationRequestPage.tsx | 360 ++++ .../pages/CollectibleUsernameDetailPage.tsx | 253 +++ .../src/pages/CollectibleUsernamesPage.tsx | 309 ++++ cmd/telesrv-admin/web/src/pages/LoginPage.tsx | 7 +- cmd/telesrv-admin/web/src/pages/Routes.tsx | 63 + .../web/src/pages/VerificationDetailPage.tsx | 412 +++++ .../web/src/pages/VerificationPage.tsx | 232 +++ cmd/telesrv-admin/web/src/permissions.tsx | 75 + cmd/telesrv-admin/web/src/routing.ts | 10 + .../web/src/styles/02-pages-and-forms.css | 56 + .../src/styles/03-entities-and-actions.css | 126 +- cmd/telesrv-admin/web/src/types.ts | 351 ++++ cmd/telesrv/main.go | 324 +++- .../0151_collectible_usernames.down.sql | 45 + .../0151_collectible_usernames.up.sql | 194 ++ .../migrations/0152_account_rating.down.sql | 3 + deploy/migrations/0152_account_rating.up.sql | 73 + .../0153_verify_service_bot.down.sql | 9 + .../migrations/0153_verify_service_bot.up.sql | 89 + .../0154_verification_applications.down.sql | 3 + .../0154_verification_applications.up.sql | 138 ++ .../migrations/0155_bot_verification.down.sql | 4 + .../migrations/0155_bot_verification.up.sql | 140 ++ .../0156_verifier_service_bot.down.sql | 12 + .../0156_verifier_service_bot.up.sql | 103 ++ .../0157_custom_emoji_reactions.down.sql | 23 + .../0157_custom_emoji_reactions.up.sql | 28 + ...0158_drop_service_account_ratings.down.sql | 5 + .../0158_drop_service_account_ratings.up.sql | 20 + docs/bot_verification.md | 359 ++++ docs/configuration.en.md | 112 ++ docs/configuration.zh-CN.md | 52 + docs/verification.md | 291 +++ internal/admin/botverification.go | 994 ++++++++++ internal/admin/botverification_test.go | 1045 +++++++++++ internal/admin/service.go | 761 +++++++- internal/admin/service_test.go | 662 +++++++ internal/admin/verification.go | 529 ++++++ internal/admin/verification_test.go | 629 +++++++ internal/adminapi/botverification.go | 561 ++++++ internal/adminapi/botverification_test.go | 826 +++++++++ internal/adminapi/rbac.go | 196 ++ internal/adminapi/server.go | 485 ++++- internal/adminapi/server_test.go | 366 ++++ internal/adminapi/verification.go | 361 ++++ internal/adminapi/verification_test.go | 546 ++++++ internal/app/bots/botfather.go | 35 +- internal/app/bots/service.go | 111 ++ internal/app/bots/verifierbot.go | 1626 +++++++++++++++++ internal/app/bots/verifierbot_test.go | 1012 ++++++++++ internal/app/bots/verifybot.go | 1493 +++++++++++++++ internal/app/bots/verifybot_test.go | 984 ++++++++++ internal/app/botverification/service.go | 1527 ++++++++++++++++ internal/app/botverification/service_test.go | 1539 ++++++++++++++++ internal/app/help/service.go | 4 +- internal/app/help/service_premium_test.go | 67 +- internal/app/rating/service.go | 453 +++++ internal/app/rating/service_test.go | 719 ++++++++ internal/app/rating/worker.go | 91 + internal/app/usernames/service.go | 552 ++++++ internal/app/usernames/service_test.go | 734 ++++++++ internal/app/verification/service.go | 1422 ++++++++++++++ internal/app/verification/service_test.go | 1416 ++++++++++++++ internal/app/verification/worker.go | 88 + internal/config/config.go | 426 +++++ internal/config/config_test.go | 355 ++++ internal/domain/account_rating.go | 398 ++++ internal/domain/bot_verification.go | 369 ++++ internal/domain/collectible_username.go | 635 +++++++ internal/domain/system.go | 76 +- internal/domain/verification.go | 651 +++++++ internal/rpc/account.go | 68 +- internal/rpc/bot_verification_flags_test.go | 616 +++++++ internal/rpc/bot_verification_notify.go | 107 ++ internal/rpc/bot_verification_notify_test.go | 335 ++++ internal/rpc/bot_verification_projection.go | 314 ++++ internal/rpc/bot_verification_rpc_test.go | 537 ++++++ internal/rpc/bots_callback.go | 19 + internal/rpc/bots_errors.go | 25 + internal/rpc/bots_longtail.go | 97 +- internal/rpc/channels_core.go | 10 +- internal/rpc/channels_invites.go | 36 +- .../rpc/channels_invites_verified_test.go | 146 ++ internal/rpc/channels_members.go | 6 +- internal/rpc/channels_settings.go | 44 +- internal/rpc/channels_state_mutation.go | 9 +- internal/rpc/channels_stubs.go | 18 +- internal/rpc/channels_updates.go | 12 +- .../rpc/collectible_usernames_rpc_test.go | 709 +++++++ internal/rpc/contacts.go | 4 +- internal/rpc/contacts_users_rpc_test.go | 4 +- internal/rpc/convert_channels_core.go | 1 - internal/rpc/convert_users.go | 44 +- internal/rpc/deps.go | 85 + internal/rpc/fragment.go | 350 ++++ internal/rpc/photos.go | 2 +- internal/rpc/premium_sweeper.go | 13 +- internal/rpc/presence.go | 9 + internal/rpc/router.go | 1 + internal/rpc/stats.go | 2 +- internal/rpc/story_peer_projection.go | 63 +- internal/rpc/username_notify.go | 64 + internal/rpc/username_notify_test.go | 94 + internal/rpc/users.go | 35 +- internal/rpc/verification_notify.go | 117 ++ internal/rpc/verification_notify_test.go | 319 ++++ internal/store/account_rating.go | 49 + internal/store/bot_verification.go | 97 + internal/store/collectible_username.go | 60 + internal/store/memory/account_rating.go | 392 ++++ internal/store/memory/account_rating_test.go | 425 +++++ internal/store/memory/bot_verification.go | 1088 +++++++++++ .../store/memory/bot_verification_test.go | 851 +++++++++ internal/store/memory/collectible_username.go | 687 +++++++ .../store/memory/collectible_username_test.go | 845 +++++++++ internal/store/memory/verification.go | 1008 ++++++++++ internal/store/memory/verification_test.go | 760 ++++++++ internal/store/postgres/account_lifecycle.go | 2 +- internal/store/postgres/account_rating.go | 557 ++++++ .../account_rating_integration_test.go | 462 +++++ internal/store/postgres/bot.go | 4 +- internal/store/postgres/bot_verification.go | 1416 ++++++++++++++ .../bot_verification_integration_test.go | 1068 +++++++++++ ...nnel_member_index_plan_integration_test.go | 8 +- internal/store/postgres/channel_settings.go | 16 +- .../channel_username_integration_test.go | 4 +- .../store/postgres/collectible_username.go | 750 ++++++++ .../collectible_username_integration_test.go | 614 +++++++ internal/store/postgres/message_send.go | 10 +- internal/store/postgres/peer_username.go | 192 +- ...ft_lifecycle_migration_integration_test.go | 4 +- ...ift_resale_seller_sync_integration_test.go | 266 +++ internal/store/postgres/user.go | 23 +- internal/store/postgres/verification.go | 1342 ++++++++++++++ .../postgres/verification_integration_test.go | 967 ++++++++++ internal/store/verification.go | 89 + 169 files changed, 55769 insertions(+), 282 deletions(-) create mode 100644 cmd/telesrv-admin/botverification.go create mode 100644 cmd/telesrv-admin/botverification_test.go create mode 100644 cmd/telesrv-admin/readstore_accounts_integration_test.go create mode 100644 cmd/telesrv-admin/readstore_botverification_integration_test.go create mode 100644 cmd/telesrv-admin/readstore_verification_integration_test.go create mode 100644 cmd/telesrv-admin/security.go create mode 100644 cmd/telesrv-admin/verification.go create mode 100644 cmd/telesrv-admin/verification_test.go delete mode 100644 cmd/telesrv-admin/web/dist/assets/index-D5Lc7N2D.css delete mode 100644 cmd/telesrv-admin/web/dist/assets/index-DJw3UpEg.js create mode 100644 cmd/telesrv-admin/web/dist/assets/index-D_BLAfeq.js create mode 100644 cmd/telesrv-admin/web/dist/assets/index-KZOn7Xwd.css create mode 100644 cmd/telesrv-admin/web/src/pages/AccountRatingDetailPage.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/AccountRatingsPage.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/BotVerificationPage.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/BotVerificationRequestPage.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/CollectibleUsernameDetailPage.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/CollectibleUsernamesPage.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/VerificationDetailPage.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/VerificationPage.tsx create mode 100644 cmd/telesrv-admin/web/src/permissions.tsx create mode 100644 deploy/migrations/0151_collectible_usernames.down.sql create mode 100644 deploy/migrations/0151_collectible_usernames.up.sql create mode 100644 deploy/migrations/0152_account_rating.down.sql create mode 100644 deploy/migrations/0152_account_rating.up.sql create mode 100644 deploy/migrations/0153_verify_service_bot.down.sql create mode 100644 deploy/migrations/0153_verify_service_bot.up.sql create mode 100644 deploy/migrations/0154_verification_applications.down.sql create mode 100644 deploy/migrations/0154_verification_applications.up.sql create mode 100644 deploy/migrations/0155_bot_verification.down.sql create mode 100644 deploy/migrations/0155_bot_verification.up.sql create mode 100644 deploy/migrations/0156_verifier_service_bot.down.sql create mode 100644 deploy/migrations/0156_verifier_service_bot.up.sql create mode 100644 deploy/migrations/0157_custom_emoji_reactions.down.sql create mode 100644 deploy/migrations/0157_custom_emoji_reactions.up.sql create mode 100644 deploy/migrations/0158_drop_service_account_ratings.down.sql create mode 100644 deploy/migrations/0158_drop_service_account_ratings.up.sql create mode 100644 docs/bot_verification.md create mode 100644 docs/verification.md create mode 100644 internal/admin/botverification.go create mode 100644 internal/admin/botverification_test.go create mode 100644 internal/admin/verification.go create mode 100644 internal/admin/verification_test.go create mode 100644 internal/adminapi/botverification.go create mode 100644 internal/adminapi/botverification_test.go create mode 100644 internal/adminapi/rbac.go create mode 100644 internal/adminapi/verification.go create mode 100644 internal/adminapi/verification_test.go create mode 100644 internal/app/bots/verifierbot.go create mode 100644 internal/app/bots/verifierbot_test.go create mode 100644 internal/app/bots/verifybot.go create mode 100644 internal/app/bots/verifybot_test.go create mode 100644 internal/app/botverification/service.go create mode 100644 internal/app/botverification/service_test.go create mode 100644 internal/app/rating/service.go create mode 100644 internal/app/rating/service_test.go create mode 100644 internal/app/rating/worker.go create mode 100644 internal/app/usernames/service.go create mode 100644 internal/app/usernames/service_test.go create mode 100644 internal/app/verification/service.go create mode 100644 internal/app/verification/service_test.go create mode 100644 internal/app/verification/worker.go create mode 100644 internal/domain/account_rating.go create mode 100644 internal/domain/bot_verification.go create mode 100644 internal/domain/collectible_username.go create mode 100644 internal/domain/verification.go create mode 100644 internal/rpc/bot_verification_flags_test.go create mode 100644 internal/rpc/bot_verification_notify.go create mode 100644 internal/rpc/bot_verification_notify_test.go create mode 100644 internal/rpc/bot_verification_projection.go create mode 100644 internal/rpc/bot_verification_rpc_test.go create mode 100644 internal/rpc/channels_invites_verified_test.go create mode 100644 internal/rpc/collectible_usernames_rpc_test.go create mode 100644 internal/rpc/fragment.go create mode 100644 internal/rpc/username_notify.go create mode 100644 internal/rpc/username_notify_test.go create mode 100644 internal/rpc/verification_notify.go create mode 100644 internal/rpc/verification_notify_test.go create mode 100644 internal/store/account_rating.go create mode 100644 internal/store/bot_verification.go create mode 100644 internal/store/collectible_username.go create mode 100644 internal/store/memory/account_rating.go create mode 100644 internal/store/memory/account_rating_test.go create mode 100644 internal/store/memory/bot_verification.go create mode 100644 internal/store/memory/bot_verification_test.go create mode 100644 internal/store/memory/collectible_username.go create mode 100644 internal/store/memory/collectible_username_test.go create mode 100644 internal/store/memory/verification.go create mode 100644 internal/store/memory/verification_test.go create mode 100644 internal/store/postgres/account_rating.go create mode 100644 internal/store/postgres/account_rating_integration_test.go create mode 100644 internal/store/postgres/bot_verification.go create mode 100644 internal/store/postgres/bot_verification_integration_test.go create mode 100644 internal/store/postgres/collectible_username.go create mode 100644 internal/store/postgres/collectible_username_integration_test.go create mode 100644 internal/store/postgres/star_gift_resale_seller_sync_integration_test.go create mode 100644 internal/store/postgres/verification.go create mode 100644 internal/store/postgres/verification_integration_test.go create mode 100644 internal/store/verification.go diff --git a/.env.example b/.env.example index 770c50b7..48aaa223 100644 --- a/.env.example +++ b/.env.example @@ -125,6 +125,21 @@ TELESRV_ADMIN_API_ADDR= # Admin UI 监听地址,默认值通常无需修改;RTMP ingest 保留 2400。 TELESRV_ADMIN_UI_ADDR=127.0.0.1:2600 +# Permissions granted to an Admin UI session that logged in with +# TELESRV_ADMIN_UI_PASSWORD / _TOKEN. Comma-separated; "*" means every +# permission and is the default, so enabling RBAC never locks an operator out of +# a panel that worked before. Names are letters/digits/._:- and may end in +# "namespace.*" to grant a whole namespace. +TELESRV_ADMIN_UI_PERMISSIONS=* + +# Additional Admin API bearer tokens with a bounded permission set each, so an +# integration gets exactly the rights it needs instead of the unrestricted +# TELESRV_ADMIN_API_TOKEN. Format: "name:token:perm1,perm2" entries separated by +# ';'. A token may not contain ':' or whitespace, names and tokens must be +# unique, and reusing TELESRV_ADMIN_API_TOKEN here is refused; any malformed +# entry fails startup rather than silently granting or dropping rights. +TELESRV_ADMIN_SCOPED_TOKENS= + TELESRV_POSTGRES_DSN=postgres://telesrv:telesrv@127.0.0.1:5432/telesrv?sslmode=disable TELESRV_REDIS_ADDR=127.0.0.1:6399 TELESRV_REDIS_PASSWORD= @@ -167,6 +182,90 @@ TELESRV_STARGIFT_TRANSFER_DELAY=0s TELESRV_STARGIFT_RESELL_DELAY=0s TELESRV_STARGIFT_CRAFT_DELAY=0s TELESRV_STARGIFT_CRAFT_CHANCE_PERMILLE=250 + +# Local admin-only composite account rating. It is not projected into Telegram's +# userFull.stars_rating fields. Disabling it refuses local rating writes. +TELESRV_RATING_ENABLED=true +# A local rating increase is parked for this long before it becomes the visible +# admin level; a decrease always applies immediately. 0 applies every change at once. +TELESRV_RATING_PENDING_DELAY=24h +# Background recompute worker: the rating derives from signals owned by other +# subsystems, so freshness is a worker property rather than a write-path one. +TELESRV_RATING_RECOMPUTE_INTERVAL=15m +TELESRV_RATING_RECOMPUTE_BATCH=500 +TELESRV_RATING_STALE_AFTER=6h +# Integer composite weights; the defaults below are exactly the shipped domain +# formula. Penalties are magnitudes that the formula subtracts, so every value is +# non-negative and a negative one fails startup. +TELESRV_RATING_WEIGHT_STARS_RECEIVED_PERMILLE=1000 +TELESRV_RATING_WEIGHT_STARS_SPENT_PERMILLE=250 +TELESRV_RATING_WEIGHT_MESSAGE_SENT=1 +TELESRV_RATING_WEIGHT_ACCOUNT_AGE_DAY=2 +TELESRV_RATING_WEIGHT_GIFT_RECEIVED=25 +TELESRV_RATING_WEIGHT_MODERATION_CASE=150 +TELESRV_RATING_WEIGHT_SCAM_PENALTY=5000 +TELESRV_RATING_WEIGHT_FAKE_PENALTY=5000 +# Upper bound of the activity component so activity alone cannot outweigh Stars +# and moderation; 0 leaves it uncapped. +TELESRV_RATING_ACTIVITY_CAP=5000 + +# Landing URL recorded on a minted collectible (NFT) username when the mint +# command carries no explicit URL. Empty derives +# /nft/username/. A template may carry the +# {username} placeholder; without it the name is appended as the last path +# segment. No external marketplace is contacted. +TELESRV_COLLECTIBLE_USERNAME_URL_TEMPLATE= + +# Official platform verification: applications filed through the built-in +# @verifybot and decided in the admin panel. An approval flips the platform +# verified flag on the target peer and nothing else; it is not the third-party +# bot verification icon. Disabling refuses every verification use case, while +# peers already carrying the badge keep it. +TELESRV_VERIFICATION_ENABLED=true +# Plain user accounts as verification subjects. Off by default: the official +# process verifies a public presence (bot, public channel, public supergroup). +TELESRV_VERIFICATION_ALLOW_USER_TARGETS=false +# How long an applicant must wait before filing the same target again after a +# rejection, measured from the decision so a slow review never shortens it. +# 0 disables the cooldown; must be 0..8760h. +TELESRV_VERIFICATION_REJECT_COOLDOWN=720h +# Applications one applicant may create per window. Either value 0 disables the +# budget; a positive limit requires a positive window. +TELESRV_VERIFICATION_APPLY_RATE_LIMIT=3 +TELESRV_VERIFICATION_APPLY_RATE_WINDOW=24h +# @verifybot dialog rate per applicant, independent of how many applications are +# actually created. Either value 0 disables it. +TELESRV_VERIFICATION_BOT_RATE_LIMIT=30 +TELESRV_VERIFICATION_BOT_RATE_WINDOW=1m +# Applicant notification worker. A decision commits with its outbox row, never +# with a message send, so delivery is a separate retrying cycle over durable +# rows. Interval must be positive; batch must be 1..500. +TELESRV_VERIFICATION_NOTIFY_INTERVAL=15s +TELESRV_VERIFICATION_NOTIFY_BATCH=50 +# Applications one applicant may keep open at once; 0 disables the cap, maximum +# is 50. +TELESRV_VERIFICATION_MAX_ACTIVE_PER_USER=3 + +# Third-party bot verification (core.telegram.org/api/bots/verification): a +# verifier bot marks peers with its OWN icon and description, which clients render +# before the name. This is NOT the platform checkmark above: the operator grants +# verifier status to a bot, and the two mechanisms never read each other's state. +# Disabling refuses every third-party mutation (grants, revocations, applications, +# icon catalogue edits) while the marks already granted keep rendering -- blanking +# one verifier's badges is what its per-verifier kill switch is for. +TELESRV_BOT_VERIFICATION_ENABLED=true +# Peers one verifier bot may mark. Verifier status is granted per deployment rather +# than earned per peer, so an unbounded verifier would be an unbounded badge +# printer. 0 disables the service bound and leaves only the storage bound, which is +# also the maximum accepted here (10000). +TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER=10000 +# Verification applications one applicant may file per window, across all verifier +# bots. Either value 0 disables the budget; a positive limit requires a positive +# window. Looser than the official budget on purpose: a deployment can run several +# verifier companies, and filing with a second one is not a retry of the first. +TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT=5 +TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW=24h + TELESRV_BLOB_DIR=data/blobs TELESRV_STICKER_SEED_DIR=data/sticker-seed # Optional Premium feature-preview media export. Missing directory keeps the diff --git a/cmd/telesrv-admin/botverification.go b/cmd/telesrv-admin/botverification.go new file mode 100644 index 00000000..8b2b53ef --- /dev/null +++ b/cmd/telesrv-admin/botverification.go @@ -0,0 +1,569 @@ +package main + +import ( + "errors" + "net/http" + "strconv" + "strings" + + "telesrv/internal/admin" + "telesrv/internal/domain" +) + +// Third-party bot verification in the panel BFF +// (core.telegram.org/api/bots/verification). +// +// This is NOT the official platform badge (see verification.go): third-party +// verification is an attributed mark granted by a verifier bot, carrying that +// verifier's own custom emoji icon and description. The two mechanisms own +// separate tables (verification_icons / bot_verifier_settings / +// custom_verifications / custom_verification_requests vs +// verification_applications), separate permissions (botverification.* vs +// verification.*) and separate routes, and neither reads the other's state. +// +// Reads come straight from PostgreSQL, like every other table view, so the tables +// page without a hop through the admin API and peers can be resolved by a join. +// Every mutation goes the other way -- always through the admin API, so the command +// journal, the status machine and the optimistic lock are enforced in one place and +// a panel action is indistinguishable from an API one in the audit trail. + +// botVerificationRead mounts a route behind a session and botverification.review. +func (s *server) botVerificationRead(handler http.HandlerFunc) http.Handler { + return s.requireAuthAPI(s.requirePermission(permissionBotVerificationReview, handler)) +} + +// botVerificationManage mounts a route behind a session and botverification.manage. +// +// The manage right is checked on its own rather than on top of review: appointing +// a verifier and working its queue are different jobs, so an operator may hold +// either without the other. +func (s *server) botVerificationManage(handler http.HandlerFunc) http.Handler { + return s.requireAuthAPI(s.requirePermission(permissionBotVerificationManage, handler)) +} + +// --------------------------------------------------------------------------- +// Reads +// --------------------------------------------------------------------------- + +func (s *server) handleBotVerifiersAPI(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + limit, err := parseInt(query.Get("limit")) + if err != nil || limit < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid limit") + return + } + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + rows, err := s.read.ListBotVerifiers(r.Context(), queryFlag(query.Get("enabled_only")), limit) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{"rows": rows}) +} + +func (s *server) handleVerificationIconsAPI(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + limit, err := parseInt(query.Get("limit")) + if err != nil || limit < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid limit") + return + } + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + rows, err := s.read.ListVerificationIcons(r.Context(), queryFlag(query.Get("active_only")), limit) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{"rows": rows}) +} + +// handleCustomVerificationsAPI pages granted marks. The filter is validated before +// the read store is consulted: a malformed query is a 400 whether or not the +// database happens to be reachable. +func (s *server) handleCustomVerificationsAPI(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + peerType := strings.TrimSpace(query.Get("peer_type")) + if !validMarkablePeerType(peerType) { + writeAPIError(w, http.StatusBadRequest, "invalid peer_type") + return + } + verifierBotID, err := parseInt64(query.Get("verifier_bot_id")) + if err != nil || verifierBotID < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid verifier_bot_id") + return + } + beforeID, err := parseInt64(query.Get("before_id")) + if err != nil || beforeID < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid before_id") + return + } + limit, err := parseInt(query.Get("limit")) + if err != nil || limit < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid limit") + return + } + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + rows, hasMore, err := s.read.ListCustomVerifications(r.Context(), verifierBotID, peerType, query.Get("q"), beforeID, limit) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + nextBeforeID := "" + if hasMore && len(rows) > 0 { + nextBeforeID = strconv.FormatInt(rows[len(rows)-1].ID, 10) + } + writeJSON(w, http.StatusOK, map[string]any{ + "rows": rows, + "has_more": hasMore, + "next_before_id": nextBeforeID, + }) +} + +func (s *server) handleCustomVerificationRequestsAPI(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + status := strings.TrimSpace(query.Get("status")) + if status != "" && !domain.CustomVerificationRequestStatus(status).Valid() { + writeAPIError(w, http.StatusBadRequest, "invalid status") + return + } + peerType := strings.TrimSpace(query.Get("peer_type")) + if !validMarkablePeerType(peerType) { + writeAPIError(w, http.StatusBadRequest, "invalid peer_type") + return + } + verifierBotID, err := parseInt64(query.Get("verifier_bot_id")) + if err != nil || verifierBotID < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid verifier_bot_id") + return + } + beforeID, err := parseInt64(query.Get("before_id")) + if err != nil || beforeID < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid before_id") + return + } + limit, err := parseInt(query.Get("limit")) + if err != nil || limit < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid limit") + return + } + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + rows, hasMore, err := s.read.ListCustomVerificationRequests( + r.Context(), status, verifierBotID, peerType, query.Get("q"), beforeID, limit, + ) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + nextBeforeID := "" + if hasMore && len(rows) > 0 { + nextBeforeID = strconv.FormatInt(rows[len(rows)-1].ID, 10) + } + writeJSON(w, http.StatusOK, map[string]any{ + "rows": rows, + "has_more": hasMore, + "next_before_id": nextBeforeID, + }) +} + +func (s *server) handleCustomVerificationRequestDetailAPI(w http.ResponseWriter, r *http.Request) { + id, ok := botVerificationPathID(w, r) + if !ok { + return + } + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + detail, err := s.read.CustomVerificationRequestDetail(r.Context(), id) + if err != nil { + if errors.Is(err, errReadNotFound) { + writeAPIError(w, http.StatusNotFound, "custom verification request not found") + return + } + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{ + "request": detail.Request, + "verifier": detail.Verifier, + // mark_active describes the peer as it is now, not as the status implies: a + // reviewer has to see that an approved mark was since stripped by the + // operator before deciding anything else about it. + "mark_active": detail.MarkActive, + }) +} + +func (s *server) handleCustomVerificationCountsAPI(w http.ResponseWriter, r *http.Request) { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + counts, err := s.read.CustomVerificationRequestCounts(r.Context()) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{"counts": counts}) +} + +// --------------------------------------------------------------------------- +// Queue decisions +// --------------------------------------------------------------------------- + +// botVerificationDecisionAPIRequest is the decision payload shared by the three +// per-application actions. version is the optimistic-locking token the reviewer +// read; internal_note is operator-only and is not part of what the applicant is +// told. It is optional everywhere, so one panel form can drive all three actions +// without tripping the strict decoder. +type botVerificationDecisionAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + Version flexInt64 `json:"version"` + InternalNote string `json:"internal_note"` +} + +func (s *server) handleApproveBotVerificationAPI(w http.ResponseWriter, r *http.Request) { + id, ok := botVerificationPathID(w, r) + if !ok { + return + } + var body botVerificationDecisionAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.ApproveBotVerificationRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "approve-bot-verification"), + RequestID: id, + Version: body.Version.Int64(), + InternalNote: body.InternalNote, + } + result, status, err := s.callAdminCommand(r.Context(), botVerificationDecisionPath(id, "approve"), req) + writeBotVerificationResultAPI(w, result, status, err) +} + +func (s *server) handleRejectBotVerificationAPI(w http.ResponseWriter, r *http.Request) { + id, ok := botVerificationPathID(w, r) + if !ok { + return + } + var body botVerificationDecisionAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.RejectBotVerificationRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "reject-bot-verification"), + RequestID: id, + Version: body.Version.Int64(), + InternalNote: body.InternalNote, + } + result, status, err := s.callAdminCommand(r.Context(), botVerificationDecisionPath(id, "reject"), req) + writeBotVerificationResultAPI(w, result, status, err) +} + +func (s *server) handleRevokeBotVerificationAPI(w http.ResponseWriter, r *http.Request) { + id, ok := botVerificationPathID(w, r) + if !ok { + return + } + var body botVerificationDecisionAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.RevokeBotVerificationRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "revoke-bot-verification"), + RequestID: id, + Version: body.Version.Int64(), + InternalNote: body.InternalNote, + } + result, status, err := s.callAdminCommand(r.Context(), botVerificationDecisionPath(id, "revoke"), req) + writeBotVerificationResultAPI(w, result, status, err) +} + +// --------------------------------------------------------------------------- +// Operator actions +// --------------------------------------------------------------------------- + +// grantBotVerifierAPIRequest appoints a bot as a verifier or reconfigures one. +// version is 0 for a new grant and the token the operator read for an update, so +// two operators editing the same verifier cannot clobber each other. enabled is +// deliberately absent: the kill switch is its own action. +type grantBotVerifierAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + BotID flexInt64 `json:"bot_id"` + IconDocumentID flexInt64 `json:"icon_document_id"` + CompanyName string `json:"company_name"` + DefaultDescription string `json:"default_description"` + CanModifyCustomDescription bool `json:"can_modify_custom_description"` + Version flexInt64 `json:"version"` +} + +func (s *server) handleGrantBotVerifierAPI(w http.ResponseWriter, r *http.Request) { + var body grantBotVerifierAPIRequest + if !decodeAction(w, r, &body) { + return + } + if body.BotID.Int64() <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid bot_id") + return + } + if body.IconDocumentID.Int64() <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid icon_document_id") + return + } + if strings.TrimSpace(body.CompanyName) == "" { + writeAPIError(w, http.StatusBadRequest, "company_name is required") + return + } + if body.Version.Int64() < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid version") + return + } + req := admin.GrantBotVerifierRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "grant-bot-verifier"), + BotID: body.BotID.Int64(), + IconDocumentID: body.IconDocumentID.Int64(), + CompanyName: body.CompanyName, + DefaultDescription: body.DefaultDescription, + CanModifyCustomDescription: body.CanModifyCustomDescription, + Version: body.Version.Int64(), + } + result, status, err := s.callAdminCommand(r.Context(), "/v1/botverification/verifiers/grant", req) + writeBotVerificationResultAPI(w, result, status, err) +} + +type setBotVerifierEnabledAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + BotID flexInt64 `json:"bot_id"` + Enabled bool `json:"enabled"` +} + +func (s *server) handleSetBotVerifierEnabledAPI(w http.ResponseWriter, r *http.Request) { + var body setBotVerifierEnabledAPIRequest + if !decodeAction(w, r, &body) { + return + } + if body.BotID.Int64() <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid bot_id") + return + } + req := admin.SetBotVerifierEnabledRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-bot-verifier-enabled"), + BotID: body.BotID.Int64(), + Enabled: body.Enabled, + } + result, status, err := s.callAdminCommand(r.Context(), "/v1/botverification/verifiers/set-enabled", req) + writeBotVerificationResultAPI(w, result, status, err) +} + +type revokeBotVerifierAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + BotID flexInt64 `json:"bot_id"` +} + +func (s *server) handleRevokeBotVerifierAPI(w http.ResponseWriter, r *http.Request) { + var body revokeBotVerifierAPIRequest + if !decodeAction(w, r, &body) { + return + } + if body.BotID.Int64() <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid bot_id") + return + } + req := admin.RevokeBotVerifierRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "revoke-bot-verifier"), + BotID: body.BotID.Int64(), + } + result, status, err := s.callAdminCommand(r.Context(), "/v1/botverification/verifiers/revoke", req) + writeBotVerificationResultAPI(w, result, status, err) +} + +// upsertVerificationIconAPIRequest adds or updates a catalogue entry. owner_bot_id +// is optional: absent (or 0) means a shared entry any verifier may use. +type upsertVerificationIconAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + DocumentID flexInt64 `json:"document_id"` + Name string `json:"name"` + OwnerBotID flexInt64 `json:"owner_bot_id"` +} + +func (s *server) handleUpsertVerificationIconAPI(w http.ResponseWriter, r *http.Request) { + var body upsertVerificationIconAPIRequest + if !decodeAction(w, r, &body) { + return + } + if body.DocumentID.Int64() <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid document_id") + return + } + if strings.TrimSpace(body.Name) == "" { + writeAPIError(w, http.StatusBadRequest, "name is required") + return + } + if body.OwnerBotID.Int64() < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid owner_bot_id") + return + } + req := admin.UpsertVerificationIconRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "upsert-verification-icon"), + DocumentID: body.DocumentID.Int64(), + Name: body.Name, + OwnerBotID: body.OwnerBotID.Int64(), + } + result, status, err := s.callAdminCommand(r.Context(), "/v1/botverification/icons/upsert", req) + writeBotVerificationResultAPI(w, result, status, err) +} + +type setVerificationIconActiveAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + IconID flexInt64 `json:"icon_id"` + Active bool `json:"active"` +} + +func (s *server) handleSetVerificationIconActiveAPI(w http.ResponseWriter, r *http.Request) { + var body setVerificationIconActiveAPIRequest + if !decodeAction(w, r, &body) { + return + } + if body.IconID.Int64() <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid icon_id") + return + } + req := admin.SetVerificationIconActiveRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-verification-icon-active"), + IconID: body.IconID.Int64(), + Active: body.Active, + } + result, status, err := s.callAdminCommand(r.Context(), "/v1/botverification/icons/set-active", req) + writeBotVerificationResultAPI(w, result, status, err) +} + +// revokeCustomVerificationAPIRequest strips one verifier's mark from a peer. It +// addresses the (verifier, peer) pair rather than an application, because the +// operator may have to strip a mark no application ever produced. +type revokeCustomVerificationAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + VerifierBotID flexInt64 `json:"verifier_bot_id"` + PeerType string `json:"peer_type"` + PeerID flexInt64 `json:"peer_id"` +} + +func (s *server) handleRevokeCustomVerificationAPI(w http.ResponseWriter, r *http.Request) { + var body revokeCustomVerificationAPIRequest + if !decodeAction(w, r, &body) { + return + } + if body.VerifierBotID.Int64() <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid verifier_bot_id") + return + } + peerType := strings.TrimSpace(body.PeerType) + if peerType == "" || !validMarkablePeerType(peerType) { + writeAPIError(w, http.StatusBadRequest, "invalid peer_type") + return + } + if body.PeerID.Int64() <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid peer_id") + return + } + req := admin.RevokeCustomVerificationRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "revoke-custom-verification"), + VerifierBotID: body.VerifierBotID.Int64(), + PeerType: domain.PeerType(peerType), + PeerID: body.PeerID.Int64(), + } + result, status, err := s.callAdminCommand(r.Context(), "/v1/botverification/marks/revoke", req) + writeBotVerificationResultAPI(w, result, status, err) +} + +// --------------------------------------------------------------------------- +// Shared helpers +// --------------------------------------------------------------------------- + +func botVerificationPathID(w http.ResponseWriter, r *http.Request) (int64, bool) { + id, err := parseInt64(r.PathValue("id")) + if err != nil || id <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid id") + return 0, false + } + return id, true +} + +func botVerificationDecisionPath(requestID int64, action string) string { + return "/v1/botverification/requests/" + strconv.FormatInt(requestID, 10) + "/" + action +} + +// validMarkablePeerType accepts the peer kinds a third-party mark can sit on, plus +// the empty string for "no filter". An unmodelled value is refused rather than +// silently returning nothing, so a typo is reported. +func validMarkablePeerType(peerType string) bool { + switch domain.PeerType(peerType) { + case "", domain.PeerTypeUser, domain.PeerTypeChannel: + return true + default: + return false + } +} + +// queryFlag reads a boolean query flag the way the panel writes it. +func queryFlag(raw string) bool { + switch strings.ToLower(strings.TrimSpace(raw)) { + case "1", "true", "yes", "on": + return true + default: + return false + } +} + +// writeBotVerificationResultAPI relays the admin API's own status to the browser. +// +// The generic action handlers flatten every upstream failure into 502, which is +// fine when the only failure mode is "bad request". These have more: 409 when +// another operator changed the row first or a verifier hit its mark bound, and 404 +// for a row that is gone. Those have to reach the panel intact, because 409 is the +// one failure it resolves by reloading rather than by asking the operator to change +// something. +func writeBotVerificationResultAPI(w http.ResponseWriter, result admin.CommandResult, status int, err error) { + if err == nil { + writeJSON(w, http.StatusOK, result) + return + } + if result.Status == "" { + result.Status = "failed" + } + if result.Message == "" { + result.Message = "command failed" + } + if result.Error == "" { + result.Error = err.Error() + } + if status < 400 { + // No HTTP answer at all: the admin API was unreachable or unparsable. + status = http.StatusBadGateway + } + writeJSON(w, status, result) +} diff --git a/cmd/telesrv-admin/botverification_test.go b/cmd/telesrv-admin/botverification_test.go new file mode 100644 index 00000000..741a6a50 --- /dev/null +++ b/cmd/telesrv-admin/botverification_test.go @@ -0,0 +1,559 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "telesrv/internal/admin" +) + +// Third-party bot verification in the panel BFF. The section is separate from the +// official verification one in every dimension that matters here: its own routes, +// its own two permissions, and no overlap with verification.* in either direction. + +// botVerificationRoute is one panel route with a body its handler accepts. +type panelBotVerificationRoute struct { + method string + path string + body string +} + +var panelBotVerificationReadRoutes = []panelBotVerificationRoute{ + {http.MethodGet, "/api/botverification/verifiers", ""}, + {http.MethodGet, "/api/botverification/icons", ""}, + {http.MethodGet, "/api/botverification/marks", ""}, + {http.MethodGet, "/api/botverification/requests", ""}, + {http.MethodGet, "/api/botverification/requests/7", ""}, + {http.MethodGet, "/api/botverification/counts", ""}, + {http.MethodPost, "/api/botverification/requests/7/approve", `{}`}, + {http.MethodPost, "/api/botverification/requests/7/reject", `{}`}, + {http.MethodPost, "/api/botverification/requests/7/revoke", `{}`}, +} + +var panelBotVerificationManageRoutes = []panelBotVerificationRoute{ + {http.MethodPost, "/api/actions/grant-bot-verifier", `{}`}, + {http.MethodPost, "/api/actions/set-bot-verifier-enabled", `{}`}, + {http.MethodPost, "/api/actions/revoke-bot-verifier", `{}`}, + {http.MethodPost, "/api/actions/upsert-verification-icon", `{}`}, + {http.MethodPost, "/api/actions/set-verification-icon-active", `{}`}, + {http.MethodPost, "/api/actions/revoke-custom-verification", `{}`}, +} + +func panelBotVerificationRoutes() []panelBotVerificationRoute { + out := make([]panelBotVerificationRoute, 0, + len(panelBotVerificationReadRoutes)+len(panelBotVerificationManageRoutes)) + out = append(out, panelBotVerificationReadRoutes...) + return append(out, panelBotVerificationManageRoutes...) +} + +func TestBotVerificationPanelRoutesRequireASession(t *testing.T) { + srv := panelServer(t, permissionAll) + for _, item := range panelBotVerificationRoutes() { + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, httptest.NewRequest(item.method, item.path, strings.NewReader(`{}`))) + if rec.Code != http.StatusUnauthorized { + t.Fatalf("%s %s status=%d, want 401", item.method, item.path, rec.Code) + } + } +} + +// Every mutating route in the section is behind the double-submit CSRF token, like +// every other one in the panel: a cookie-authenticated request forged by another +// origin must not be able to appoint a verifier. +func TestBotVerificationMutationsRequireTheCSRFHeader(t *testing.T) { + srv := panelServer(t, permissionAll) + cookies, token := signIn(t, srv) + for _, item := range panelBotVerificationRoutes() { + if item.method != http.MethodPost { + continue + } + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies( + httptest.NewRequest(item.method, item.path, strings.NewReader(item.body)), cookies)) + if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), csrfHeaderName) { + t.Fatalf("%s status=%d body=%s, want 403 without a csrf header", item.path, rec.Code, rec.Body.String()) + } + } + // A foreign origin is refused even when the token is right. + req := withCookies(httptest.NewRequest(http.MethodPost, "/api/actions/grant-bot-verifier", + strings.NewReader(`{}`)), cookies) + req.Header.Set(csrfHeaderName, token) + req.Header.Set("Origin", "https://evil.example") + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), "origin") { + t.Fatalf("foreign origin status=%d body=%s, want 403", rec.Code, rec.Body.String()) + } +} + +// Reads do not need the token: they change nothing, and requiring it would break +// the panel without adding protection. +func TestBotVerificationReadsDoNotNeedTheCSRFHeader(t *testing.T) { + srv := panelServer(t, permissionBotVerificationReview) + cookies, _ := signIn(t, srv) + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies( + httptest.NewRequest(http.MethodGet, "/api/botverification/verifiers", nil), cookies)) + // No read store is wired in this fixture, so the gate passing is what is under + // test: 503 means the request got past authorisation and CSRF. + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("GET status=%d body=%s, want the gate passed without a token", rec.Code, rec.Body.String()) + } +} + +func TestBotVerificationRoutesRefuseASessionWithoutTheRight(t *testing.T) { + // A session holding only the OFFICIAL verification rights: the two mechanisms + // are separate, so it must not reach this section at all. + srv := panelServer(t, permissionVerificationReview, permissionVerificationRevoke) + cookies, token := signIn(t, srv) + + check := func(item panelBotVerificationRoute, wantPermission string) { + var req *http.Request + if item.body == "" { + req = httptest.NewRequest(item.method, item.path, nil) + } else { + req = httptest.NewRequest(item.method, item.path, strings.NewReader(item.body)) + req.Header.Set(csrfHeaderName, token) + } + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies(req, cookies)) + if rec.Code != http.StatusForbidden { + t.Fatalf("%s %s status=%d body=%s, want 403", item.method, item.path, rec.Code, rec.Body.String()) + } + var body map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("decode 403 body: %v", err) + } + if body["code"] != "FORBIDDEN" || body["permission"] != wantPermission { + t.Fatalf("%s 403 body=%+v, want %s named", item.path, body, wantPermission) + } + } + for _, item := range panelBotVerificationReadRoutes { + check(item, permissionBotVerificationReview) + } + for _, item := range panelBotVerificationManageRoutes { + check(item, permissionBotVerificationManage) + } +} + +// The two halves are independent: the review right does not appoint verifiers, and +// the manage right does not decide applications. +func TestBotVerificationReviewAndManageAreIndependent(t *testing.T) { + reviewOnly := panelServer(t, permissionBotVerificationReview) + cookies, token := signIn(t, reviewOnly) + req := withCookies(httptest.NewRequest(http.MethodPost, "/api/actions/grant-bot-verifier", + strings.NewReader(`{"reason":"partner","confirm":true,"bot_id":3003,"icon_document_id":900,"company_name":"x"}`)), cookies) + req.Header.Set(csrfHeaderName, token) + rec := httptest.NewRecorder() + reviewOnly.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), permissionBotVerificationManage) { + t.Fatalf("review-only on grant status=%d body=%s, want 403 naming manage", rec.Code, rec.Body.String()) + } + + manageOnly := panelServer(t, permissionBotVerificationManage) + cookies, token = signIn(t, manageOnly) + req = withCookies(httptest.NewRequest(http.MethodPost, "/api/botverification/requests/7/approve", + strings.NewReader(`{"reason":"verified","confirm":true,"version":3}`)), cookies) + req.Header.Set(csrfHeaderName, token) + rec = httptest.NewRecorder() + manageOnly.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), permissionBotVerificationReview) { + t.Fatalf("manage-only on approve status=%d body=%s, want 403 naming review", rec.Code, rec.Body.String()) + } +} + +// A session holding the third-party rights must not reach the official section +// either: the separation is symmetric. +func TestBotVerificationSessionCannotReachTheOfficialSection(t *testing.T) { + srv := panelServer(t, permissionBotVerificationReview, permissionBotVerificationManage) + cookies, _ := signIn(t, srv) + for _, path := range []string{"/api/verification/applications", "/api/verification/counts"} { + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies(httptest.NewRequest(http.MethodGet, path, nil), cookies)) + if rec.Code != http.StatusForbidden { + t.Fatalf("%s status=%d body=%s, want 403", path, rec.Code, rec.Body.String()) + } + } +} + +func TestApproveBotVerificationBFFForwardsActorVersionAndNote(t *testing.T) { + upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed"}} + api := httptest.NewServer(upstream.handler(t)) + defer api.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/botverification/requests/88/approve", strings.NewReader(`{ + "reason":"the outlet checks out","confirm":true,"version":"9223372036854775807", + "internal_note":"contact came through the press office" + }`)) + req.SetPathValue("id", "88") + req = requestWithActor(req, "operator") + rec := httptest.NewRecorder() + srv.handleApproveBotVerificationAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + if upstream.path != "/v1/botverification/requests/88/approve" { + t.Fatalf("upstream path=%q", upstream.path) + } + var got admin.ApproveBotVerificationRequest + if err := json.Unmarshal(upstream.raw, &got); err != nil { + t.Fatalf("decode forwarded approval: %v (%s)", err, upstream.raw) + } + if got.Actor != "operator" { + t.Fatalf("actor=%q, want the signed-in operator", got.Actor) + } + // The version arrives as a decimal string from the browser and must survive + // exactly: a rounded version would decide the wrong revision of the row. + if got.RequestID != 88 || got.Version != 9223372036854775807 { + t.Fatalf("forwarded approval=%+v, want the exact int64 version", got) + } + if got.InternalNote != "contact came through the press office" || got.DryRun { + t.Fatalf("forwarded approval=%+v", got) + } + if got.CommandID == "" { + t.Fatal("no command id was minted for the idempotency key") + } +} + +// confirm=false is a rehearsal: nothing may be written until the operator confirms. +func TestBotVerificationBFFDefaultsToADryRun(t *testing.T) { + upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed", DryRun: true}} + api := httptest.NewServer(upstream.handler(t)) + defer api.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/botverification/requests/88/reject", strings.NewReader( + `{"reason":"not an outlet","confirm":false,"version":3}`)) + req.SetPathValue("id", "88") + req = requestWithActor(req, "operator") + rec := httptest.NewRecorder() + srv.handleRejectBotVerificationAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + var got admin.RejectBotVerificationRequest + if err := json.Unmarshal(upstream.raw, &got); err != nil { + t.Fatalf("decode forwarded rejection: %v", err) + } + if !got.DryRun || got.Version != 3 || got.RequestID != 88 { + t.Fatalf("forwarded rejection=%+v", got) + } + + // The same on an operator action. + req = requestWithActor(httptest.NewRequest(http.MethodPost, "/api/actions/revoke-bot-verifier", strings.NewReader( + `{"reason":"programme ended","confirm":false,"bot_id":3003}`)), "operator") + rec = httptest.NewRecorder() + srv.handleRevokeBotVerifierAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("revoke status=%d body=%s", rec.Code, rec.Body.String()) + } + var revoke admin.RevokeBotVerifierRequest + if err := json.Unmarshal(upstream.raw, &revoke); err != nil { + t.Fatalf("decode forwarded revocation: %v", err) + } + if !revoke.DryRun || revoke.BotID != 3003 { + t.Fatalf("forwarded revocation=%+v", revoke) + } +} + +func TestGrantBotVerifierBFFForwardsThePayloadAndRejectsBadShapes(t *testing.T) { + upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed"}} + api := httptest.NewServer(upstream.handler(t)) + defer api.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}} + req := requestWithActor(httptest.NewRequest(http.MethodPost, "/api/actions/grant-bot-verifier", strings.NewReader(`{ + "reason":"partner programme","confirm":true, + "bot_id":"9223372036854775807","icon_document_id":"9223372036854775806", + "company_name":"Example Trust","default_description":"verified by Example Trust", + "can_modify_custom_description":true,"version":"4" + }`)), "operator") + rec := httptest.NewRecorder() + srv.handleGrantBotVerifierAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + if upstream.path != "/v1/botverification/verifiers/grant" { + t.Fatalf("upstream path=%q", upstream.path) + } + var got admin.GrantBotVerifierRequest + if err := json.Unmarshal(upstream.raw, &got); err != nil { + t.Fatalf("decode forwarded grant: %v", err) + } + if got.BotID != 9223372036854775807 || got.IconDocumentID != 9223372036854775806 || + got.Version != 4 || got.Actor != "operator" || got.DryRun { + t.Fatalf("forwarded grant=%+v, want the exact int64s", got) + } + if got.CompanyName != "Example Trust" || !got.CanModifyCustomDescription { + t.Fatalf("forwarded grant=%+v", got) + } + + for _, payload := range []string{ + `{"reason":"x","confirm":true,"bot_id":0,"icon_document_id":900,"company_name":"y"}`, + `{"reason":"x","confirm":true,"bot_id":3003,"icon_document_id":0,"company_name":"y"}`, + `{"reason":"x","confirm":true,"bot_id":3003,"icon_document_id":900,"company_name":" "}`, + `{"reason":"x","confirm":true,"bot_id":3003,"icon_document_id":900,"company_name":"y","version":-1}`, + } { + rec := httptest.NewRecorder() + srv.handleGrantBotVerifierAPI(rec, requestWithActor( + httptest.NewRequest(http.MethodPost, "/api/actions/grant-bot-verifier", strings.NewReader(payload)), "operator")) + if rec.Code != http.StatusBadRequest { + t.Fatalf("payload %s status=%d body=%s, want 400", payload, rec.Code, rec.Body.String()) + } + } +} + +func TestBotVerificationOperatorActionsForwardTheirPayloads(t *testing.T) { + upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed"}} + api := httptest.NewServer(upstream.handler(t)) + defer api.Close() + srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}} + + rec := httptest.NewRecorder() + srv.handleSetBotVerifierEnabledAPI(rec, requestWithActor(httptest.NewRequest( + http.MethodPost, "/api/actions/set-bot-verifier-enabled", strings.NewReader( + `{"reason":"abuse report","confirm":true,"bot_id":"3003","enabled":false}`)), "operator")) + if rec.Code != http.StatusOK || upstream.path != "/v1/botverification/verifiers/set-enabled" { + t.Fatalf("set-enabled status=%d path=%q body=%s", rec.Code, upstream.path, rec.Body.String()) + } + var setEnabled admin.SetBotVerifierEnabledRequest + if err := json.Unmarshal(upstream.raw, &setEnabled); err != nil { + t.Fatalf("decode: %v", err) + } + if setEnabled.BotID != 3003 || setEnabled.Enabled || setEnabled.Actor != "operator" { + t.Fatalf("forwarded=%+v", setEnabled) + } + + rec = httptest.NewRecorder() + srv.handleUpsertVerificationIconAPI(rec, requestWithActor(httptest.NewRequest( + http.MethodPost, "/api/actions/upsert-verification-icon", strings.NewReader( + `{"reason":"new icon","confirm":true,"document_id":"9223372036854775807","name":"blue check","owner_bot_id":"3003"}`)), "operator")) + if rec.Code != http.StatusOK || upstream.path != "/v1/botverification/icons/upsert" { + t.Fatalf("upsert-icon status=%d path=%q body=%s", rec.Code, upstream.path, rec.Body.String()) + } + var icon admin.UpsertVerificationIconRequest + if err := json.Unmarshal(upstream.raw, &icon); err != nil { + t.Fatalf("decode: %v", err) + } + if icon.DocumentID != 9223372036854775807 || icon.Name != "blue check" || icon.OwnerBotID != 3003 { + t.Fatalf("forwarded=%+v", icon) + } + // owner_bot_id is optional: absent means a shared catalogue entry. + rec = httptest.NewRecorder() + srv.handleUpsertVerificationIconAPI(rec, requestWithActor(httptest.NewRequest( + http.MethodPost, "/api/actions/upsert-verification-icon", strings.NewReader( + `{"reason":"new icon","confirm":true,"document_id":900,"name":"shared"}`)), "operator")) + if rec.Code != http.StatusOK { + t.Fatalf("shared icon status=%d body=%s", rec.Code, rec.Body.String()) + } + // A fresh target: owner_bot_id is omitted when zero, so decoding into the + // previous value would silently keep the reserved owner. + var shared admin.UpsertVerificationIconRequest + if err := json.Unmarshal(upstream.raw, &shared); err != nil { + t.Fatalf("decode: %v", err) + } + if shared.OwnerBotID != 0 || shared.Name != "shared" { + t.Fatalf("forwarded=%+v, want a shared entry", shared) + } + + rec = httptest.NewRecorder() + srv.handleSetVerificationIconActiveAPI(rec, requestWithActor(httptest.NewRequest( + http.MethodPost, "/api/actions/set-verification-icon-active", strings.NewReader( + `{"reason":"retired","confirm":true,"icon_id":"501","active":false}`)), "operator")) + if rec.Code != http.StatusOK || upstream.path != "/v1/botverification/icons/set-active" { + t.Fatalf("set-icon-active status=%d path=%q", rec.Code, upstream.path) + } + var iconActive admin.SetVerificationIconActiveRequest + if err := json.Unmarshal(upstream.raw, &iconActive); err != nil { + t.Fatalf("decode: %v", err) + } + if iconActive.IconID != 501 || iconActive.Active { + t.Fatalf("forwarded=%+v", iconActive) + } + + rec = httptest.NewRecorder() + srv.handleRevokeCustomVerificationAPI(rec, requestWithActor(httptest.NewRequest( + http.MethodPost, "/api/actions/revoke-custom-verification", strings.NewReader( + `{"reason":"impersonation","confirm":true,"verifier_bot_id":"3003","peer_type":"channel","peer_id":"9223372036854775807"}`)), "operator")) + if rec.Code != http.StatusOK || upstream.path != "/v1/botverification/marks/revoke" { + t.Fatalf("revoke-mark status=%d path=%q body=%s", rec.Code, upstream.path, rec.Body.String()) + } + var mark admin.RevokeCustomVerificationRequest + if err := json.Unmarshal(upstream.raw, &mark); err != nil { + t.Fatalf("decode: %v", err) + } + if mark.VerifierBotID != 3003 || mark.PeerType != "channel" || mark.PeerID != 9223372036854775807 { + t.Fatalf("forwarded=%+v", mark) + } + + for _, payload := range []string{ + `{"reason":"x","confirm":true,"verifier_bot_id":0,"peer_type":"channel","peer_id":5}`, + `{"reason":"x","confirm":true,"verifier_bot_id":3003,"peer_type":"chat","peer_id":5}`, + `{"reason":"x","confirm":true,"verifier_bot_id":3003,"peer_type":"","peer_id":5}`, + `{"reason":"x","confirm":true,"verifier_bot_id":3003,"peer_type":"channel","peer_id":0}`, + } { + rec := httptest.NewRecorder() + srv.handleRevokeCustomVerificationAPI(rec, requestWithActor(httptest.NewRequest( + http.MethodPost, "/api/actions/revoke-custom-verification", strings.NewReader(payload)), "operator")) + if rec.Code != http.StatusBadRequest { + t.Fatalf("payload %s status=%d body=%s, want 400", payload, rec.Code, rec.Body.String()) + } + } +} + +// A body may not smuggle in an actor: the signed-in operator is the audit identity, +// and the strict decoder is what enforces it. +func TestBotVerificationRequestsRejectUnknownFields(t *testing.T) { + srv := &server{cfg: uiConfig{AdminAPIURL: "http://127.0.0.1:1", AdminAPIToken: "api-secret"}} + + req := httptest.NewRequest(http.MethodPost, "/api/botverification/requests/88/approve", strings.NewReader( + `{"reason":"ok","confirm":true,"version":3,"actor":"attacker"}`)) + req.SetPathValue("id", "88") + rec := httptest.NewRecorder() + srv.handleApproveBotVerificationAPI(rec, requestWithActor(req, "operator")) + if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "actor") { + t.Fatalf("status=%d body=%s, want 400 rejecting the injected actor", rec.Code, rec.Body.String()) + } + + // enabled is not part of the grant form: the kill switch is its own action, and + // a silently ignored field would hide that from the operator. + rec = httptest.NewRecorder() + srv.handleGrantBotVerifierAPI(rec, requestWithActor(httptest.NewRequest( + http.MethodPost, "/api/actions/grant-bot-verifier", strings.NewReader( + `{"reason":"x","confirm":true,"bot_id":3003,"icon_document_id":900,"company_name":"y","enabled":true}`)), "operator")) + if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "enabled") { + t.Fatalf("status=%d body=%s, want 400 naming the unknown field", rec.Code, rec.Body.String()) + } +} + +func TestBotVerificationPathIDIsValidated(t *testing.T) { + srv := &server{cfg: uiConfig{AdminAPIURL: "http://127.0.0.1:1", AdminAPIToken: "api-secret"}} + for _, id := range []string{"", "0", "-1", "abc"} { + req := httptest.NewRequest(http.MethodPost, "/api/botverification/requests/x/approve", strings.NewReader( + `{"reason":"ok","confirm":true,"version":3}`)) + req.SetPathValue("id", id) + rec := httptest.NewRecorder() + srv.handleApproveBotVerificationAPI(rec, requestWithActor(req, "operator")) + if rec.Code != http.StatusBadRequest { + t.Fatalf("id=%q status=%d, want 400", id, rec.Code) + } + } +} + +// A flattened 502 would hide the one failure the panel resolves by reloading. +func TestBotVerificationConflictReachesThePanelAs409(t *testing.T) { + upstream := &verificationUpstream{ + status: http.StatusConflict, + body: admin.CommandResult{ + CommandID: "c1", Status: "failed", + Error: admin.CodeCustomVerificationConflict + ": custom verification changed concurrently", + Message: "another operator changed this row first; reload it and try again", + }, + } + api := httptest.NewServer(upstream.handler(t)) + defer api.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/botverification/requests/88/approve", strings.NewReader( + `{"reason":"ok","confirm":true,"version":3}`)) + req.SetPathValue("id", "88") + rec := httptest.NewRecorder() + srv.handleApproveBotVerificationAPI(rec, requestWithActor(req, "operator")) + if rec.Code != http.StatusConflict { + t.Fatalf("status=%d body=%s, want 409", rec.Code, rec.Body.String()) + } + var result admin.CommandResult + if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil { + t.Fatalf("decode conflict: %v", err) + } + if !strings.Contains(result.Error, admin.CodeCustomVerificationConflict) || + !strings.Contains(result.Message, "reload") { + t.Fatalf("result=%+v", result) + } + + // The manage half too: two operators can race one verifier row. + rec = httptest.NewRecorder() + srv.handleGrantBotVerifierAPI(rec, requestWithActor(httptest.NewRequest( + http.MethodPost, "/api/actions/grant-bot-verifier", strings.NewReader( + `{"reason":"x","confirm":true,"bot_id":3003,"icon_document_id":900,"company_name":"y","version":3}`)), "operator")) + if rec.Code != http.StatusConflict { + t.Fatalf("grant status=%d body=%s, want 409", rec.Code, rec.Body.String()) + } + + // A 404 from upstream is preserved as well, so a decision on a row that is gone + // is not reported as an upstream outage. + upstream.status = http.StatusNotFound + upstream.body = admin.CommandResult{CommandID: "c2", Status: "failed", + Error: admin.CodeCustomVerificationRequestNotFound + ": custom verification request not found"} + req = httptest.NewRequest(http.MethodPost, "/api/botverification/requests/88/reject", strings.NewReader( + `{"reason":"ok","confirm":true,"version":3}`)) + req.SetPathValue("id", "88") + rec = httptest.NewRecorder() + srv.handleRejectBotVerificationAPI(rec, requestWithActor(req, "operator")) + if rec.Code != http.StatusNotFound { + t.Fatalf("status=%d body=%s, want 404", rec.Code, rec.Body.String()) + } +} + +// An unreachable admin API is the one case with no upstream status at all. +func TestBotVerificationUnreachableUpstreamIs502(t *testing.T) { + srv := &server{cfg: uiConfig{AdminAPIURL: "http://127.0.0.1:1", AdminAPIToken: "api-secret"}} + rec := httptest.NewRecorder() + srv.handleRevokeBotVerifierAPI(rec, requestWithActor(httptest.NewRequest( + http.MethodPost, "/api/actions/revoke-bot-verifier", strings.NewReader( + `{"reason":"x","confirm":true,"bot_id":3003}`)), "operator")) + if rec.Code != http.StatusBadGateway { + t.Fatalf("status=%d body=%s, want 502", rec.Code, rec.Body.String()) + } +} + +func TestBotVerificationReadFiltersAreValidatedBeforeTheStore(t *testing.T) { + // No read store: a malformed query still has to be a 400, so the panel is told + // what it got wrong whether or not the database is reachable. + srv := &server{} + cases := []struct { + handler http.HandlerFunc + path string + }{ + {srv.handleCustomVerificationsAPI, "/api/botverification/marks?peer_type=chat"}, + {srv.handleCustomVerificationsAPI, "/api/botverification/marks?verifier_bot_id=abc"}, + {srv.handleCustomVerificationsAPI, "/api/botverification/marks?before_id=-1"}, + {srv.handleCustomVerificationsAPI, "/api/botverification/marks?limit=abc"}, + {srv.handleCustomVerificationRequestsAPI, "/api/botverification/requests?status=in_review"}, + {srv.handleCustomVerificationRequestsAPI, "/api/botverification/requests?peer_type=chat"}, + {srv.handleCustomVerificationRequestsAPI, "/api/botverification/requests?limit=-1"}, + {srv.handleBotVerifiersAPI, "/api/botverification/verifiers?limit=abc"}, + {srv.handleVerificationIconsAPI, "/api/botverification/icons?limit=-2"}, + } + for _, item := range cases { + rec := httptest.NewRecorder() + item.handler(rec, httptest.NewRequest(http.MethodGet, item.path, nil)) + if rec.Code != http.StatusBadRequest { + t.Fatalf("%s status=%d body=%s, want 400", item.path, rec.Code, rec.Body.String()) + } + } + // A well-formed query with no store wired reports the store, not the query. + rec := httptest.NewRecorder() + srv.handleCustomVerificationRequestsAPI(rec, httptest.NewRequest( + http.MethodGet, "/api/botverification/requests?status=pending&peer_type=channel&limit=10", nil)) + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("status=%d body=%s, want 503", rec.Code, rec.Body.String()) + } +} + +func TestQueryFlagReadsThePanelsBooleans(t *testing.T) { + for _, raw := range []string{"1", "true", "TRUE", " yes ", "on"} { + if !queryFlag(raw) { + t.Fatalf("queryFlag(%q) = false", raw) + } + } + for _, raw := range []string{"", "0", "false", "no", "maybe"} { + if queryFlag(raw) { + t.Fatalf("queryFlag(%q) = true", raw) + } + } +} diff --git a/cmd/telesrv-admin/main.go b/cmd/telesrv-admin/main.go index 953b79a2..597d84db 100644 --- a/cmd/telesrv-admin/main.go +++ b/cmd/telesrv-admin/main.go @@ -71,6 +71,11 @@ type uiConfig struct { Password string Token string SessionKey []byte + // Permissions is the right set a panel session is issued with, from + // TELESRV_ADMIN_UI_PERMISSIONS. The shipped default is the single wildcard + // entry, so introducing the permission model never locks an operator out of a + // panel that worked before. + Permissions []string } // loadConfig 通过 internal/config.Load() 加载 .env 配置文件与环境变量, @@ -105,6 +110,7 @@ func loadConfig() (uiConfig, error) { Password: appCfg.AdminUIPassword, Token: appCfg.AdminUIToken, SessionKey: sum[:], + Permissions: appCfg.AdminUIPermissions, }, nil } diff --git a/cmd/telesrv-admin/readstore.go b/cmd/telesrv-admin/readstore.go index 0d169fa9..71e251e7 100644 --- a/cmd/telesrv-admin/readstore.go +++ b/cmd/telesrv-admin/readstore.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "strconv" "strings" @@ -22,8 +23,38 @@ const ( channelListDefaultLimit = 50 channelListMaxLimit = 100 messagePageLimit = 100 + // Collectible username and account rating pages. The bounds mirror the + // use-case layer, so a table page costs the same whichever surface asks. + collectibleListDefaultLimit = 50 + collectibleListMaxLimit = 200 + collectibleTransferLimit = 50 + ratingListDefaultLimit = 50 + ratingListMaxLimit = 200 + ratingEventLimit = 50 + // Verification review queue pages. The bounds mirror app/verification, so the + // panel and the admin API page the queue identically. + verificationListDefaultLimit = 50 + verificationListMaxLimit = 200 + verificationEventLimit = 100 + // Third-party bot verification pages. The bounds mirror app/botverification, so + // the panel and the admin API page the verifier tables identically. + botVerificationListDefaultLimit = 50 + botVerificationListMaxLimit = 200 ) +// errReadNotFound reports a detail row that does not exist, so the API layer can +// answer 404 without importing the driver's sentinel. +var errReadNotFound = errors.New("read row not found") + +// escapeLikePattern neutralises LIKE metacharacters in an operator query. +// Usernames legitimately contain '_', so an unescaped search for "crypto_" would +// silently match "cryptoX" instead of the name the operator typed. +func escapeLikePattern(value string) string { + replaced := strings.ReplaceAll(value, `\`, `\\`) + replaced = strings.ReplaceAll(replaced, "%", `\%`) + return strings.ReplaceAll(replaced, "_", `\_`) +} + type readStore struct { pool *pgxpool.Pool } @@ -32,12 +63,27 @@ func newReadStore(pool *pgxpool.Pool) *readStore { return &readStore{pool: pool} } +// AccountUsername is one collectible (Fragment-style) username a peer holds. +// +// Active mirrors the username#b4073647 flag: an inactive collectible is still +// owned, it just does not resolve publicly, and an operator has to be able to +// tell those two apart -- so the row is listed either way and carries the flag +// rather than being filtered out. +type AccountUsername struct { + Username string + Active bool +} + type AccountRow struct { - ID int64 - Phone string - Username string - FirstName string - LastName string + ID int64 + Phone string + Username string + FirstName string + LastName string + // Collectibles are the peer's collectible usernames in the order clients + // project them. The editable slot in Username is never repeated here: it is a + // different kind of row that a different RPC owns. + Collectibles []AccountUsername CreatedAt time.Time UpdatedAt time.Time Frozen bool @@ -50,6 +96,22 @@ type AccountRow struct { DeviceCount int } +// accountCollectibleUsernamesColumn aggregates a peer's collectible usernames +// into one jsonb value, so a list page costs one indexed subquery per row instead +// of a second round trip per account. +// +// The object keys are the AccountUsername field names on purpose: pgx unmarshals +// jsonb straight into the Go value, and matching the field names keeps the panel's +// JSON shape identical to every other field on the row (PascalCase) instead of +// introducing one lowercase island. Ordering matches domain.SortUsernames for +// collectible rows -- stored order, then the name as a stable tiebreak. +const accountCollectibleUsernamesColumn = `COALESCE(( + SELECT jsonb_agg(jsonb_build_object('Username', pc.username, 'Active', pc.active) + ORDER BY pc.sort_order, pc.username_lower) + FROM peer_usernames pc + WHERE pc.peer_type = 'user' AND pc.peer_id = u.id AND pc.collectible_id IS NOT NULL +), '[]'::jsonb)` + type AccountDetail struct { Account AccountRow About string @@ -107,15 +169,15 @@ type AuditLogRow struct { } type ChannelRow struct { - ID int64 - AccessHash int64 - CreatorUserID int64 - Title string - About string - Username string - Broadcast bool - Megagroup bool - Forum bool + ID int64 + AccessHash int64 + CreatorUserID int64 + Title string + About string + Username string + Broadcast bool + Megagroup bool + Forum bool Monoforum bool Verified bool Scam bool @@ -129,15 +191,15 @@ type ChannelRow struct { JoinRequest bool SlowmodeSeconds int ParticipantsCount int - AdminsCount int - KickedCount int - BannedCount int - TopMessageID int - PinnedMessageID int - PTS int - Date int - CreatedAt time.Time - UpdatedAt time.Time + AdminsCount int + KickedCount int + BannedCount int + TopMessageID int + PinnedMessageID int + PTS int + Date int + CreatedAt time.Time + UpdatedAt time.Time } type ChannelDetail struct { @@ -225,7 +287,7 @@ SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.upd COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username FROM users u 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 +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable LEFT JOIN auth a ON a.user_id = u.id WHERE u.id = $1 OR u.phone = $2 OR u.phone = $3 OR lower(u.username) = $4 OR p.username_lower = $4 ORDER BY u.id @@ -281,7 +343,7 @@ SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_nam COALESCE(b.owner_user_id, 0), u.created_at, u.updated_at FROM users u LEFT JOIN bots b ON b.bot_user_id = u.id -LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable WHERE u.is_bot AND u.deleted_at IS NULL AND ($1::bigint = 0 OR u.id < $1) ORDER BY u.id DESC LIMIT $2`, beforeID, limit+1) @@ -323,7 +385,7 @@ SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_nam COALESCE(b.owner_user_id, 0), u.created_at, u.updated_at FROM users u LEFT JOIN bots b ON b.bot_user_id = u.id -LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable WHERE u.is_bot AND u.deleted_at IS NULL AND (u.id = $1 OR lower(u.username) = $2 OR p.username_lower = $2) ORDER BY u.id DESC LIMIT $3`, id, username, accountSearchLimit) @@ -351,7 +413,7 @@ SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_nam u.created_at, u.updated_at FROM users u LEFT JOIN bots b ON b.bot_user_id = u.id -LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable WHERE u.id = $1 AND u.is_bot AND u.deleted_at IS NULL`, botUserID).Scan( &out.Bot.ID, &out.Bot.Username, &out.Bot.FirstName, &out.About, &out.Bot.Verified, &out.Bot.Scam, &out.Bot.Fake, &out.Bot.OwnerUserID, &out.Description, &out.Bot.CreatedAt, &out.Bot.UpdatedAt, @@ -365,7 +427,7 @@ WHERE u.id = $1 AND u.is_bot AND u.deleted_at IS NULL`, botUserID).Scan( if err := s.pool.QueryRow(ctx, ` SELECT COALESCE(NULLIF(u.username, ''), p.username_lower, '') FROM users u -LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable WHERE u.id = $1`, out.Bot.OwnerUserID).Scan(&ownerUsername); err != nil && err != pgx.ErrNoRows { return out, fmt.Errorf("get bot owner: %w", err) } else { @@ -397,7 +459,7 @@ SELECT c.id, c.access_hash, c.creator_user_id, c.title, c.about, c.participants_count, c.admins_count, c.kicked_count, c.banned_count, c.top_message_id, c.pinned_message_id, c.pts, c.date, c.created_at, c.updated_at FROM channels c -LEFT JOIN peer_usernames p ON p.peer_type = 'channel' AND p.peer_id = c.id +LEFT JOIN peer_usernames p ON p.peer_type = 'channel' AND p.peer_id = c.id AND p.editable WHERE NOT c.deleted AND NOT c.monoforum AND (c.broadcast OR c.megagroup) @@ -431,7 +493,7 @@ SELECT c.id, c.access_hash, c.creator_user_id, c.title, c.about, c.participants_count, c.admins_count, c.kicked_count, c.banned_count, c.top_message_id, c.pinned_message_id, c.pts, c.date, c.created_at, c.updated_at FROM channels c -LEFT JOIN peer_usernames p ON p.peer_type = 'channel' AND p.peer_id = c.id +LEFT JOIN peer_usernames p ON p.peer_type = 'channel' AND p.peer_id = c.id AND p.editable WHERE NOT c.deleted AND NOT c.monoforum AND (c.broadcast OR c.megagroup) @@ -465,7 +527,7 @@ SELECT c.id, c.access_hash, c.creator_user_id, c.title, c.about, c.top_message_id, c.pinned_message_id, c.pts, c.date, c.created_at, c.updated_at, row_to_json(c)::jsonb FROM channels c -LEFT JOIN peer_usernames p ON p.peer_type = 'channel' AND p.peer_id = c.id +LEFT JOIN peer_usernames p ON p.peer_type = 'channel' AND p.peer_id = c.id AND p.editable WHERE c.id = $1 AND NOT c.deleted AND NOT c.monoforum @@ -533,11 +595,12 @@ SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.upd 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(NULLIF(u.username, ''), p.username_lower, '') AS display_username + COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username, + `+accountCollectibleUsernamesColumn+` AS collectibles FROM users u 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 +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable 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 @@ -549,7 +612,7 @@ LIMIT $3`, beforeActiveUS, beforeID, limit+1) out := make([]AccountRow, 0, limit+1) for rows.Next() { var item AccountRow - if err := rows.Scan(&item.ID, &item.Phone, &item.Username, &item.FirstName, &item.LastName, &item.CreatedAt, &item.UpdatedAt, &item.Frozen, &item.Reason, &item.Verified, &item.Scam, &item.Fake, &item.PremiumUntil, &item.LastActiveAt, &item.DeviceCount, &item.Username); err != nil { + if err := rows.Scan(&item.ID, &item.Phone, &item.Username, &item.FirstName, &item.LastName, &item.CreatedAt, &item.UpdatedAt, &item.Frozen, &item.Reason, &item.Verified, &item.Scam, &item.Fake, &item.PremiumUntil, &item.LastActiveAt, &item.DeviceCount, &item.Username, &item.Collectibles); err != nil { return nil, false, err } out = append(out, item) @@ -572,15 +635,17 @@ SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.upd COALESCE(r.frozen, false), COALESCE(r.reason, ''), COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint, COALESCE(sb.balance, 0)::bigint, COALESCE(sb.granted, false), - COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username + COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username, + `+accountCollectibleUsernamesColumn+` AS collectibles FROM users u LEFT JOIN account_restrictions r ON r.user_id = u.id LEFT JOIN stars_balances sb ON sb.user_id = u.id -LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable WHERE u.id = $1`, userID).Scan( &out.Account.ID, &out.Account.Phone, &out.Account.Username, &out.Account.FirstName, &out.Account.LastName, &out.Account.CreatedAt, &out.Account.UpdatedAt, &out.About, &out.LastSeenAt, &out.Verified, &out.Scam, &out.Fake, &out.Support, &out.Bot, &out.Account.Frozen, &out.Account.Reason, &out.Account.PremiumUntil, &out.StarsBalance, &out.StarsGranted, &out.Account.Username, + &out.Account.Collectibles, ) if err != nil { return out, fmt.Errorf("get account: %w", err) @@ -1068,3 +1133,1253 @@ LIMIT $3`, id, q, emojiListMaxLimit) defer rows.Close() return scanEmojiRows(rows) } + +// CollectibleUsernameRow is one collectible (Fragment-style) username asset with +// its holder resolved for display. +// +// Every int64 is tagged as a JSON string: asset ids, nanoton amounts and the +// optimistic-concurrency version all exceed the range a JSON number represents +// exactly, and a rounded id would address the wrong asset. +type CollectibleUsernameRow struct { + ID int64 `json:"ID,string"` + Username string + Status string + OwnerPeerType string + OwnerPeerID int64 `json:"OwnerPeerID,string"` + OwnerUsername string + OwnerName string + PurchaseDate time.Time + Currency string + Amount int64 `json:"Amount,string"` + CryptoCurrency string + CryptoAmount int64 `json:"CryptoAmount,string"` + URL string + OriginalOwnerPeerType string + OriginalOwnerPeerID int64 `json:"OriginalOwnerPeerID,string"` + OriginalOwnerUsername string + TransferCount int + Version int64 `json:"Version,string"` + CreatedAt time.Time + UpdatedAt time.Time + // RegistryActive / RegistrySortOrder mirror the holder's username registry + // row, so the panel can tell an owned-but-hidden name from an active one. + RegistryActive bool + RegistrySortOrder int +} + +// CollectibleUsernameTransferRow is one provenance log entry. +type CollectibleUsernameTransferRow struct { + ID int64 `json:"ID,string"` + CollectibleID int64 `json:"CollectibleID,string"` + Kind string + FromPeerType string + FromPeerID int64 `json:"FromPeerID,string"` + FromUsername string + ToPeerType string + ToPeerID int64 `json:"ToPeerID,string"` + ToUsername string + Currency string + Amount int64 `json:"Amount,string"` + Actor string + Reason string + CommandKey string + CreatedAt time.Time +} + +// CollectibleUsernameDetail is the asset plus its provenance log. +type CollectibleUsernameDetail struct { + Asset CollectibleUsernameRow + Transfers []CollectibleUsernameTransferRow +} + +const collectibleUsernameSelectColumns = `cu.id, cu.username, cu.status, + cu.owner_peer_type, cu.owner_peer_id, + COALESCE(NULLIF(ou.username, ''), NULLIF(oc.username, ''), '') AS owner_username, + COALESCE(NULLIF(ou.first_name, ''), NULLIF(oc.title, ''), '') AS owner_name, + cu.purchase_date, cu.currency, cu.amount, cu.crypto_currency, cu.crypto_amount, cu.url, + cu.original_owner_peer_type, cu.original_owner_peer_id, + COALESCE(NULLIF(gu.username, ''), NULLIF(gc.username, ''), '') AS original_owner_username, + cu.transfer_count, cu.version, cu.created_at, cu.updated_at, + COALESCE(pu.active, false), COALESCE(pu.sort_order, 0)` + +// collectibleUsernameJoins resolves the current holder, the original holder and +// the holder's registry row. Owners are users or channels, so both sides are +// joined and the peer type decides which one contributes. +const collectibleUsernameJoins = ` +FROM collectible_usernames cu +LEFT JOIN users ou ON cu.owner_peer_type = 'user' AND ou.id = cu.owner_peer_id +LEFT JOIN channels oc ON cu.owner_peer_type = 'channel' AND oc.id = cu.owner_peer_id +LEFT JOIN users gu ON cu.original_owner_peer_type = 'user' AND gu.id = cu.original_owner_peer_id +LEFT JOIN channels gc ON cu.original_owner_peer_type = 'channel' AND gc.id = cu.original_owner_peer_id +LEFT JOIN peer_usernames pu ON pu.collectible_id = cu.id` + +func collectibleUsernameScanDest(item *CollectibleUsernameRow) []any { + return []any{ + &item.ID, &item.Username, &item.Status, + &item.OwnerPeerType, &item.OwnerPeerID, &item.OwnerUsername, &item.OwnerName, + &item.PurchaseDate, &item.Currency, &item.Amount, &item.CryptoCurrency, &item.CryptoAmount, &item.URL, + &item.OriginalOwnerPeerType, &item.OriginalOwnerPeerID, &item.OriginalOwnerUsername, + &item.TransferCount, &item.Version, &item.CreatedAt, &item.UpdatedAt, + &item.RegistryActive, &item.RegistrySortOrder, + } +} + +// ListCollectibleUsernames pages over collectible assets newest first, keyset by +// descending id. status/ownerUserID/q are optional filters; q matches a username +// prefix, which is how an operator looks a name up. +func (s *readStore) ListCollectibleUsernames(ctx context.Context, status string, ownerUserID, beforeID int64, q string, limit int) ([]CollectibleUsernameRow, bool, error) { + if limit <= 0 { + limit = collectibleListDefaultLimit + } + if limit > collectibleListMaxLimit { + limit = collectibleListMaxLimit + } + status = strings.TrimSpace(status) + query := escapeLikePattern(strings.ToLower(strings.TrimPrefix(strings.TrimSpace(q), "@"))) + rows, err := s.pool.Query(ctx, ` +SELECT `+collectibleUsernameSelectColumns+collectibleUsernameJoins+` +WHERE ($1 = '' OR cu.status = $1) + AND ($2::bigint = 0 OR (cu.owner_peer_type = 'user' AND cu.owner_peer_id = $2)) + AND ($3 = '' OR cu.username_lower LIKE $3 || '%') + AND ($4::bigint = 0 OR cu.id < $4) +ORDER BY cu.id DESC +LIMIT $5`, status, ownerUserID, query, beforeID, limit+1) + if err != nil { + return nil, false, fmt.Errorf("list collectible usernames: %w", err) + } + defer rows.Close() + out := make([]CollectibleUsernameRow, 0, limit+1) + for rows.Next() { + var item CollectibleUsernameRow + if err := rows.Scan(collectibleUsernameScanDest(&item)...); err != nil { + return nil, false, err + } + out = append(out, item) + } + if err := rows.Err(); err != nil { + return nil, false, err + } + hasMore := len(out) > limit + if hasMore { + out = out[:limit] + } + return out, hasMore, nil +} + +// CollectibleUsernameDetail returns one asset with its provenance log. A missing +// asset reports errReadNotFound so the API answers 404 rather than 500. +func (s *readStore) CollectibleUsernameDetail(ctx context.Context, id int64) (CollectibleUsernameDetail, error) { + var out CollectibleUsernameDetail + err := s.pool.QueryRow(ctx, ` +SELECT `+collectibleUsernameSelectColumns+collectibleUsernameJoins+` +WHERE cu.id = $1`, id).Scan(collectibleUsernameScanDest(&out.Asset)...) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return out, errReadNotFound + } + return out, fmt.Errorf("get collectible username: %w", err) + } + out.Transfers, err = s.collectibleUsernameTransfers(ctx, id) + if err != nil { + return out, err + } + return out, nil +} + +func (s *readStore) collectibleUsernameTransfers(ctx context.Context, collectibleID int64) ([]CollectibleUsernameTransferRow, error) { + rows, err := s.pool.Query(ctx, ` +SELECT t.id, t.collectible_id, t.kind, + t.from_peer_type, t.from_peer_id, + COALESCE(NULLIF(fu.username, ''), NULLIF(fc.username, ''), '') AS from_username, + t.to_peer_type, t.to_peer_id, + COALESCE(NULLIF(tu.username, ''), NULLIF(tc.username, ''), '') AS to_username, + t.currency, t.amount, t.actor, t.reason, COALESCE(t.command_key, ''), t.created_at +FROM collectible_username_transfers t +LEFT JOIN users fu ON t.from_peer_type = 'user' AND fu.id = t.from_peer_id +LEFT JOIN channels fc ON t.from_peer_type = 'channel' AND fc.id = t.from_peer_id +LEFT JOIN users tu ON t.to_peer_type = 'user' AND tu.id = t.to_peer_id +LEFT JOIN channels tc ON t.to_peer_type = 'channel' AND tc.id = t.to_peer_id +WHERE t.collectible_id = $1 +ORDER BY t.id DESC +LIMIT $2`, collectibleID, collectibleTransferLimit) + if err != nil { + return nil, fmt.Errorf("list collectible username transfers: %w", err) + } + defer rows.Close() + out := make([]CollectibleUsernameTransferRow, 0) + for rows.Next() { + var item CollectibleUsernameTransferRow + if err := rows.Scan( + &item.ID, &item.CollectibleID, &item.Kind, + &item.FromPeerType, &item.FromPeerID, &item.FromUsername, + &item.ToPeerType, &item.ToPeerID, &item.ToUsername, + &item.Currency, &item.Amount, &item.Actor, &item.Reason, &item.CommandKey, &item.CreatedAt, + ); err != nil { + return nil, err + } + out = append(out, item) + } + return out, rows.Err() +} + +// AccountRatingRow is one user's composite rating projection with the account +// resolved for display. The score and every component are int64 decimal strings +// for the same exactness reason as the collectible amounts. +type AccountRatingRow struct { + UserID int64 `json:"UserID,string"` + Username string + FirstName string + Level int + Stars int64 `json:"Stars,string"` + CurrentLevelStars int64 `json:"CurrentLevelStars,string"` + NextLevelStars int64 `json:"NextLevelStars,string"` + HasNextLevel bool + StarsComponent int64 `json:"StarsComponent,string"` + ActivityComponent int64 `json:"ActivityComponent,string"` + PenaltyComponent int64 `json:"PenaltyComponent,string"` + ManualComponent int64 `json:"ManualComponent,string"` + PendingStars int64 `json:"PendingStars,string"` + PendingDate time.Time + ComputedAt time.Time + UpdatedAt time.Time + Version int64 `json:"Version,string"` + // Computed is false for an account that has no stored projection yet. The + // detail view still renders it, so the operator can trigger the first + // recompute instead of facing a dead end. + Computed bool +} + +// AccountRatingEventRow is one contribution ledger entry. +type AccountRatingEventRow struct { + ID int64 `json:"ID,string"` + UserID int64 `json:"UserID,string"` + Kind string + Amount int64 `json:"Amount,string"` + Reason string + Actor string + CommandKey string + CreatedAt time.Time +} + +// AccountRatingDetail is the projection plus the ledger that explains it. +type AccountRatingDetail struct { + Rating AccountRatingRow + Events []AccountRatingEventRow +} + +const accountRatingSelectColumns = `r.user_id, + COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username, + COALESCE(u.first_name, ''), + r.level, r.stars, r.current_level_stars, r.next_level_stars, + r.stars_component, r.activity_component, r.penalty_component, r.manual_component, + r.pending_stars, r.pending_date, r.computed_at, r.updated_at, r.version` + +const accountRatingJoins = ` +FROM account_rating r +LEFT JOIN users u ON u.id = r.user_id +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = r.user_id AND p.editable` + +func scanAccountRatingRow(scan func(dest ...any) error, item *AccountRatingRow) error { + // next_level_stars and pending_date are nullable: the first is NULL at the top + // level, the second whenever no score is parked. + var nextLevelStars *int64 + var pendingDate *time.Time + if err := scan( + &item.UserID, &item.Username, &item.FirstName, + &item.Level, &item.Stars, &item.CurrentLevelStars, &nextLevelStars, + &item.StarsComponent, &item.ActivityComponent, &item.PenaltyComponent, &item.ManualComponent, + &item.PendingStars, &pendingDate, &item.ComputedAt, &item.UpdatedAt, &item.Version, + ); err != nil { + return err + } + // A NULL next threshold is the maxed-out level: the TL flag is omitted, so the + // panel must render "no next level" instead of a next level of zero. + item.HasNextLevel = nextLevelStars != nil + if nextLevelStars != nil { + item.NextLevelStars = *nextLevelStars + } + if pendingDate != nil { + item.PendingDate = pendingDate.UTC() + } + item.Computed = true + return nil +} + +// ListAccountRatings pages the leaderboard. Ordering and the keyset predicate +// mirror the rating store exactly -- (level DESC, stars DESC, user_id) with the +// cursor row resolved from beforeID -- so both surfaces page identically. +// ListAccountRatings pages the leaderboard. query is a free-text operator search: +// it matches a username prefix (editable or collectible), a first/last name +// prefix, and -- when the term is numeric -- the user id, so an operator can find +// an account the same way they do on the accounts tab. +func (s *readStore) ListAccountRatings(ctx context.Context, minLevel int, userID, beforeID int64, limit int, query string) ([]AccountRatingRow, bool, error) { + if limit <= 0 { + limit = ratingListDefaultLimit + } + if limit > ratingListMaxLimit { + limit = ratingListMaxLimit + } + if minLevel < 0 { + minLevel = 0 + } + if minLevel > domain.MaxAccountRatingLevel { + minLevel = domain.MaxAccountRatingLevel + } + query = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(query), "@")) + pattern := "" + queryUserID := int64(0) + if query != "" { + pattern = strings.ToLower(escapeLikePattern(query)) + "%" + if parsed, err := strconv.ParseInt(query, 10, 64); err == nil && parsed > 0 { + queryUserID = parsed + } + } + rows, err := s.pool.Query(ctx, ` +WITH cursor_row AS ( + SELECT level AS c_level, stars AS c_stars, user_id AS c_user_id + FROM account_rating WHERE $3::bigint <> 0 AND user_id = $3 +) +SELECT `+accountRatingSelectColumns+accountRatingJoins+` +LEFT JOIN cursor_row c ON true +WHERE r.level >= $1 + AND ($2::bigint = 0 OR r.user_id = $2) + AND ($5::text = '' OR ( + ($6::bigint <> 0 AND r.user_id = $6) + OR lower(COALESCE(u.username, '')) LIKE $5 + OR lower(COALESCE(u.first_name, '')) LIKE $5 + OR lower(COALESCE(u.last_name, '')) LIKE $5 + OR EXISTS ( + SELECT 1 FROM peer_usernames pu + WHERE pu.peer_type = 'user' AND pu.peer_id = r.user_id + AND pu.username_lower LIKE $5 + ) + )) + AND ( + c.c_user_id IS NULL + OR r.level < c.c_level + OR (r.level = c.c_level AND r.stars < c.c_stars) + OR (r.level = c.c_level AND r.stars = c.c_stars AND r.user_id > c.c_user_id) + ) +ORDER BY r.level DESC, r.stars DESC, r.user_id +LIMIT $4`, minLevel, userID, beforeID, limit+1, pattern, queryUserID) + if err != nil { + return nil, false, fmt.Errorf("list account ratings: %w", err) + } + defer rows.Close() + out := make([]AccountRatingRow, 0, limit+1) + for rows.Next() { + var item AccountRatingRow + if err := scanAccountRatingRow(rows.Scan, &item); err != nil { + return nil, false, err + } + out = append(out, item) + } + if err := rows.Err(); err != nil { + return nil, false, err + } + hasMore := len(out) > limit + if hasMore { + out = out[:limit] + } + return out, hasMore, nil +} + +// AccountRatingDetail returns one user's projection with its contribution +// ledger. +// +// An account that exists but was never computed is answered with a zero-valued +// projection carrying Computed=false, because the recompute command lives on this +// very page: reporting "not found" for a real account would leave the operator +// with no way to create the first projection. Only an unknown account is a 404. +func (s *readStore) AccountRatingDetail(ctx context.Context, userID int64) (AccountRatingDetail, error) { + var out AccountRatingDetail + row := s.pool.QueryRow(ctx, ` +SELECT `+accountRatingSelectColumns+accountRatingJoins+` +WHERE r.user_id = $1`, userID) + err := scanAccountRatingRow(row.Scan, &out.Rating) + switch { + case err == nil: + case errors.Is(err, pgx.ErrNoRows): + placeholder, uncomputedErr := s.uncomputedAccountRating(ctx, userID) + if uncomputedErr != nil { + return out, uncomputedErr + } + out.Rating = placeholder + default: + return out, fmt.Errorf("get account rating: %w", err) + } + events, err := s.accountRatingEvents(ctx, userID) + if err != nil { + return out, err + } + out.Events = events + return out, nil +} + +// uncomputedAccountRating renders the projection an account would start from, +// derived through the same threshold policy the store persists, so the panel's +// level maths does not have to special-case a missing row. +func (s *readStore) uncomputedAccountRating(ctx context.Context, userID int64) (AccountRatingRow, error) { + var row AccountRatingRow + err := s.pool.QueryRow(ctx, ` +SELECT u.id, COALESCE(NULLIF(u.username, ''), p.username_lower, ''), u.first_name +FROM users u +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable +WHERE u.id = $1`, userID).Scan(&row.UserID, &row.Username, &row.FirstName) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return row, errReadNotFound + } + return row, fmt.Errorf("get account for rating: %w", err) + } + level, current, next, hasNext := domain.AccountRatingLevelForStars(0) + row.Level = level + row.CurrentLevelStars = current + row.NextLevelStars = next + row.HasNextLevel = hasNext + return row, nil +} + +func (s *readStore) accountRatingEvents(ctx context.Context, userID int64) ([]AccountRatingEventRow, error) { + rows, err := s.pool.Query(ctx, ` +SELECT id, user_id, kind, amount, reason, actor, COALESCE(command_key, ''), created_at +FROM account_rating_events +WHERE user_id = $1 +ORDER BY id DESC +LIMIT $2`, userID, ratingEventLimit) + if err != nil { + return nil, fmt.Errorf("list account rating events: %w", err) + } + defer rows.Close() + out := make([]AccountRatingEventRow, 0) + for rows.Next() { + var item AccountRatingEventRow + if err := rows.Scan(&item.ID, &item.UserID, &item.Kind, &item.Amount, &item.Reason, &item.Actor, &item.CommandKey, &item.CreatedAt); err != nil { + return nil, err + } + out = append(out, item) + } + return out, rows.Err() +} + +// Official platform verification review queue. +// +// The application record is the audit subject and is read here directly, with the +// applicant resolved through the same users/peer_usernames join every other view +// uses. target_verified is read from the live peer rather than from the +// submission snapshot: a reviewer has to see the badge as it is now, and the +// snapshot columns exist precisely because the live peer may have drifted. +// +// Every int64 is tagged as a JSON string. Application ids, peer ids and the +// optimistic-locking version all exceed the range a JSON number represents +// exactly, and a rounded version would send a decision against the wrong +// revision of the row. +type VerificationApplicationRow struct { + ID int64 `json:"ID,string"` + ApplicantUserID int64 `json:"ApplicantUserID,string"` + ApplicantUsername string + ApplicantName string + TargetType string + TargetID int64 `json:"TargetID,string"` + TargetTitle string + TargetUsername string + TargetVerified bool + Category string + Description string + OfficialWebsite string + SocialLinks []string + PressLinks []string + AdditionalNote string + Status string + ReviewerAdminID string + DecisionReason string + // InternalNote is operator-only: it is the reviewer handover note and is never + // projected to the applicant. Every caller of this store already holds + // verification.review. + InternalNote string + CorrelationID string + CreatedAt time.Time + UpdatedAt time.Time + SubmittedAt time.Time + ReviewedAt time.Time + Version int64 `json:"Version,string"` +} + +// VerificationEventRow is one entry of the immutable application history. +type VerificationEventRow struct { + ID int64 `json:"ID,string"` + Kind string + FromStatus string + ToStatus string + Actor string + Reason string + Note string + CreatedAt time.Time +} + +// VerificationApplicationDetail is the application, its history, and whether the +// applicant still controls the target. +type VerificationApplicationDetail struct { + Application VerificationApplicationRow + Events []VerificationEventRow + // ApplicantControlsTarget re-derives ownership from the live records, using the + // same authorities the use-case layer does: the bots table for a bot, the + // public-channel admin index for a channel or supergroup, identity for a user. + // Control can be lost between submission and review, and approving a peer the + // applicant no longer holds is exactly what the flag exists to prevent. + ApplicantControlsTarget bool +} + +const verificationSelectColumns = `va.id, va.applicant_user_id, + COALESCE(NULLIF(au.username, ''), p.username_lower, '') AS applicant_username, + TRIM(BOTH ' ' FROM COALESCE(au.first_name, '') || ' ' || COALESCE(au.last_name, '')) AS applicant_name, + va.target_type, va.target_id, va.target_title, va.target_username, + COALESCE(tu.verified, tc.verified, false) AS target_verified, + va.category, va.description, va.official_website, va.social_links, va.press_links, + va.additional_note, va.status, va.reviewer_admin_id, va.decision_reason, + va.internal_note, va.correlation_id, + va.created_at, va.updated_at, va.submitted_at, va.reviewed_at, va.version` + +// verificationJoins resolves the applicant and the live target. Bots and users +// live in the user namespace, channels and supergroups in the channel one, so +// both sides are joined and the target type decides which one contributes. +const verificationJoins = ` +FROM verification_applications va +LEFT JOIN users au ON au.id = va.applicant_user_id +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = va.applicant_user_id AND p.editable +LEFT JOIN users tu ON va.target_type IN ('bot', 'user') AND tu.id = va.target_id +LEFT JOIN channels tc ON va.target_type IN ('channel', 'supergroup') AND tc.id = va.target_id` + +func scanVerificationApplicationRow(scan func(dest ...any) error, item *VerificationApplicationRow) error { + // submitted_at is NULL while the application is still a draft and reviewed_at + // until a reviewer closes it. + var submittedAt, reviewedAt *time.Time + if err := scan( + &item.ID, &item.ApplicantUserID, &item.ApplicantUsername, &item.ApplicantName, + &item.TargetType, &item.TargetID, &item.TargetTitle, &item.TargetUsername, &item.TargetVerified, + &item.Category, &item.Description, &item.OfficialWebsite, &item.SocialLinks, &item.PressLinks, + &item.AdditionalNote, &item.Status, &item.ReviewerAdminID, &item.DecisionReason, + &item.InternalNote, &item.CorrelationID, + &item.CreatedAt, &item.UpdatedAt, &submittedAt, &reviewedAt, &item.Version, + ); err != nil { + return err + } + if submittedAt != nil { + item.SubmittedAt = submittedAt.UTC() + } + if reviewedAt != nil { + item.ReviewedAt = reviewedAt.UTC() + } + if item.SocialLinks == nil { + item.SocialLinks = []string{} + } + if item.PressLinks == nil { + item.PressLinks = []string{} + } + return nil +} + +// ListVerificationApplications pages the review queue newest first, keyset by +// descending id. status/targetType/reviewer are exact filters; q matches an +// application id, a target peer id, or a target/applicant username prefix, which +// is how an operator looks a case up from a report. +func (s *readStore) ListVerificationApplications( + ctx context.Context, + status, targetType, reviewer, q string, + beforeID int64, + limit int, +) ([]VerificationApplicationRow, bool, error) { + if limit <= 0 { + limit = verificationListDefaultLimit + } + if limit > verificationListMaxLimit { + limit = verificationListMaxLimit + } + status = strings.TrimSpace(status) + targetType = strings.TrimSpace(targetType) + reviewer = strings.TrimSpace(reviewer) + query := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(q), "@")) + pattern := "" + queryID := int64(0) + if query != "" { + pattern = strings.ToLower(escapeLikePattern(query)) + "%" + if parsed, err := strconv.ParseInt(query, 10, 64); err == nil && parsed > 0 { + queryID = parsed + } + } + rows, err := s.pool.Query(ctx, ` +SELECT `+verificationSelectColumns+verificationJoins+` +WHERE ($1 = '' OR va.status = $1) + AND ($2 = '' OR va.target_type = $2) + AND ($3 = '' OR va.reviewer_admin_id = $3) + AND ($4::bigint = 0 OR va.id < $4) + AND ($5::text = '' OR ( + ($6::bigint <> 0 AND (va.id = $6 OR va.target_id = $6 OR va.applicant_user_id = $6)) + OR lower(va.target_username) LIKE $5 + OR lower(va.target_title) LIKE $5 + OR lower(COALESCE(au.username, '')) LIKE $5 + )) +ORDER BY va.id DESC +LIMIT $7`, status, targetType, reviewer, beforeID, pattern, queryID, limit+1) + if err != nil { + return nil, false, fmt.Errorf("list verification applications: %w", err) + } + defer rows.Close() + out := make([]VerificationApplicationRow, 0, limit+1) + for rows.Next() { + var item VerificationApplicationRow + if err := scanVerificationApplicationRow(rows.Scan, &item); err != nil { + return nil, false, err + } + out = append(out, item) + } + if err := rows.Err(); err != nil { + return nil, false, err + } + hasMore := len(out) > limit + if hasMore { + out = out[:limit] + } + return out, hasMore, nil +} + +// VerificationApplicationDetail returns one application with its history and the +// live ownership check. A missing application reports errReadNotFound so the API +// answers 404 rather than 500. +func (s *readStore) VerificationApplicationDetail(ctx context.Context, id int64) (VerificationApplicationDetail, error) { + var out VerificationApplicationDetail + row := s.pool.QueryRow(ctx, ` +SELECT `+verificationSelectColumns+verificationJoins+` +WHERE va.id = $1`, id) + // The single-row path reuses the list scanner, so one column order serves + // both: a drift between them would silently mis-assign columns. + err := scanVerificationApplicationRow(row.Scan, &out.Application) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return out, errReadNotFound + } + return out, fmt.Errorf("get verification application: %w", err) + } + out.Events, err = s.verificationApplicationEvents(ctx, id) + if err != nil { + return out, err + } + controls, err := s.applicantControlsVerificationTarget( + ctx, out.Application.ApplicantUserID, out.Application.TargetType, out.Application.TargetID, + ) + if err != nil { + return out, err + } + out.ApplicantControlsTarget = controls + return out, nil +} + +func (s *readStore) verificationApplicationEvents(ctx context.Context, applicationID int64) ([]VerificationEventRow, error) { + rows, err := s.pool.Query(ctx, ` +SELECT id, kind, from_status, to_status, actor, reason, note, created_at +FROM verification_application_events +WHERE application_id = $1 +ORDER BY id DESC +LIMIT $2`, applicationID, verificationEventLimit) + if err != nil { + return nil, fmt.Errorf("list verification application events: %w", err) + } + defer rows.Close() + out := make([]VerificationEventRow, 0) + for rows.Next() { + var item VerificationEventRow + if err := rows.Scan( + &item.ID, &item.Kind, &item.FromStatus, &item.ToStatus, + &item.Actor, &item.Reason, &item.Note, &item.CreatedAt, + ); err != nil { + return nil, err + } + out = append(out, item) + } + return out, rows.Err() +} + +// VerificationStatusCounts is the queue summary above the list. Every modelled +// status is present with a zero, so the panel never has to tell "none" from +// "missing", and the counts are decimal strings for the same exactness reason as +// the ids. +func (s *readStore) VerificationStatusCounts(ctx context.Context) (map[string]string, error) { + out := map[string]string{} + for _, status := range []domain.VerificationStatus{ + domain.VerificationStatusDraft, + domain.VerificationStatusSubmitted, + domain.VerificationStatusInReview, + domain.VerificationStatusApproved, + domain.VerificationStatusRejected, + domain.VerificationStatusCancelled, + } { + out[string(status)] = "0" + } + rows, err := s.pool.Query(ctx, ` +SELECT status, count(*) FROM verification_applications GROUP BY status`) + if err != nil { + return nil, fmt.Errorf("count verification applications: %w", err) + } + defer rows.Close() + for rows.Next() { + var status string + var count int64 + if err := rows.Scan(&status, &count); err != nil { + return nil, err + } + out[status] = strconv.FormatInt(count, 10) + } + return out, rows.Err() +} + +// applicantControlsVerificationTarget re-derives ownership from the live records. +// +// The authorities are the ones app/verification uses, so the panel's answer and +// the approval path's answer cannot disagree: the bots table for a bot (minus +// BotFather, which nobody owns), the public-channel admin index for a channel or +// supergroup, and plain identity for a user account. +func (s *readStore) applicantControlsVerificationTarget(ctx context.Context, applicantUserID int64, targetType string, targetID int64) (bool, error) { + if applicantUserID <= 0 || targetID <= 0 { + return false, nil + } + switch domain.VerificationTargetType(targetType) { + case domain.VerificationTargetUser: + return applicantUserID == targetID, nil + case domain.VerificationTargetBot: + var owns bool + err := s.pool.QueryRow(ctx, ` +SELECT EXISTS ( + SELECT 1 FROM bots b + WHERE b.bot_user_id = $1 AND b.owner_user_id = $2 AND b.bot_user_id <> $3 +)`, targetID, applicantUserID, domain.BotFatherUserID).Scan(&owns) + if err != nil { + return false, fmt.Errorf("check verification bot ownership: %w", err) + } + return owns, nil + case domain.VerificationTargetChannel, domain.VerificationTargetSupergroup: + var admins bool + err := s.pool.QueryRow(ctx, ` +SELECT EXISTS ( + SELECT 1 FROM user_channel_member_index i + WHERE i.user_id = $1 AND i.channel_id = $2 + AND i.status = 'active' AND i.role IN ('creator', 'admin') + AND i.public_username AND NOT i.deleted +)`, applicantUserID, targetID).Scan(&admins) + if err != nil { + return false, fmt.Errorf("check verification channel ownership: %w", err) + } + return admins, nil + default: + return false, nil + } +} + +// Third-party bot verification (core.telegram.org/api/bots/verification). +// +// This is NOT the official platform badge read above. Official verification is a +// boolean on the peer that only the operator sets; third-party verification is an +// attributed mark granted by a verifier bot, carrying that verifier's own custom +// emoji icon and description. The two mechanisms own separate tables and neither +// reads the other's, which is why these queries never touch +// verification_applications or users.verified. +// +// Peer titles and usernames are resolved from the live peer rather than from the +// application's snapshot columns: an operator has to see the peer as it is now, +// and the snapshot is only the fallback for a peer that has since gone. Usernames +// come from the same users/channels + peer_usernames join every other view uses, +// with `AND p.editable` on the peer_usernames side -- a collectible username sits +// in the same table and is not the peer's own editable handle, so joining without +// the predicate would report somebody else's asset as the peer's name. +// +// Every int64 is tagged as a JSON string. Bot ids, peer ids, custom emoji document +// ids and the optimistic-locking version all exceed the range a JSON number +// represents exactly, and a rounded version would send a decision against the +// wrong revision of the row. + +// BotVerifierRow is one verifier bot: its operator-granted settings, the catalogue +// name of the icon it marks with, and how many peers it has marked. +type BotVerifierRow struct { + BotID int64 `json:"BotID,string"` + BotUsername string + BotName string + IconDocumentID int64 `json:"IconDocumentID,string"` + IconName string + CompanyName string + DefaultDescription string + CanModifyCustomDescription bool + Enabled bool + GrantedBy string + GrantReason string + // MarkCount is how many peers this verifier currently marks. It is the number + // that would cascade away with a revocation, so it is counted rather than + // estimated. + MarkCount int64 `json:"MarkCount,string"` + CreatedAt time.Time + UpdatedAt time.Time + Version int64 `json:"Version,string"` +} + +// VerificationIconRow is one catalogue entry. +type VerificationIconRow struct { + ID int64 `json:"ID,string"` + DocumentID int64 `json:"DocumentID,string"` + // OwnerBotID is 0 for a shared entry and a bot id when the operator reserved + // the icon for one verifier. + OwnerBotID int64 `json:"OwnerBotID,string"` + OwnerBotUsername string + Name string + Active bool + // UsedByVerifiers is a plain number: it counts verifier rows pointing at this + // document and can never approach the exactness limit an id can. + UsedByVerifiers int `json:"UsedByVerifiers"` + CreatedAt time.Time + UpdatedAt time.Time +} + +// CustomVerificationRow is one granted mark. +type CustomVerificationRow struct { + ID int64 `json:"ID,string"` + VerifierBotID int64 `json:"VerifierBotID,string"` + VerifierBotUsername string + CompanyName string + PeerType string + PeerID int64 `json:"PeerID,string"` + PeerTitle string + PeerUsername string + // IconDocumentID is the icon the mark was granted with, denormalised at grant + // time, so it keeps rendering even after the verifier changes its own. + IconDocumentID int64 `json:"IconDocumentID,string"` + Description string + CreatedAt time.Time + UpdatedAt time.Time + Version int64 `json:"Version,string"` +} + +// CustomVerificationRequestRow is one application filed with a verifier bot. +type CustomVerificationRequestRow struct { + ID int64 `json:"ID,string"` + VerifierBotID int64 `json:"VerifierBotID,string"` + VerifierBotUsername string + ApplicantUserID int64 `json:"ApplicantUserID,string"` + ApplicantUsername string + PeerType string + PeerID int64 `json:"PeerID,string"` + PeerTitle string + PeerUsername string + Reason string + RequestedDescription string + Status string + DecidedBy string + DecisionReason string + // InternalNote is operator-only: it is the reviewer handover note and is never + // projected to the applicant. Every caller of this store already holds + // botverification.review. + InternalNote string + CorrelationID string + CreatedAt time.Time + UpdatedAt time.Time + ApprovedAt time.Time + RejectedAt time.Time + Version int64 `json:"Version,string"` +} + +// CustomVerificationRequestDetail is one application, the verifier behind it, and +// whether the mark is on the peer right now. +type CustomVerificationRequestDetail struct { + Request CustomVerificationRequestRow + Verifier BotVerifierRow + // MarkActive tells "approved" apart from "approved and since stripped by the + // operator", which is the one thing the status alone cannot say. + MarkActive bool +} + +const botVerifierSelectColumns = `s.bot_id, + COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS bot_username, + TRIM(BOTH ' ' FROM COALESCE(u.first_name, '') || ' ' || COALESCE(u.last_name, '')) AS bot_name, + s.icon_document_id, COALESCE(i.name, '') AS icon_name, + s.company_name, s.default_description, s.can_modify_custom_description, + s.enabled, s.granted_by, s.grant_reason, + (SELECT count(*) FROM custom_verifications cv WHERE cv.verifier_bot_id = s.bot_id) AS mark_count, + s.created_at, s.updated_at, s.version` + +// botVerifierJoins resolves the bot account behind the verifier row and the +// catalogue label of its icon. The icon join is by document id, not by catalogue +// id: the settings row stores the document, and an icon dropped from the catalogue +// must still leave the verifier readable. +const botVerifierJoins = ` +FROM bot_verifier_settings s +LEFT JOIN users u ON u.id = s.bot_id +LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = s.bot_id AND p.editable +LEFT JOIN verification_icons i ON i.document_id = s.icon_document_id` + +func scanBotVerifierRow(scan func(dest ...any) error, item *BotVerifierRow) error { + return scan( + &item.BotID, &item.BotUsername, &item.BotName, + &item.IconDocumentID, &item.IconName, + &item.CompanyName, &item.DefaultDescription, &item.CanModifyCustomDescription, + &item.Enabled, &item.GrantedBy, &item.GrantReason, &item.MarkCount, + &item.CreatedAt, &item.UpdatedAt, &item.Version, + ) +} + +// ListBotVerifiers lists verifier bots, ordered by bot id so the table is stable +// across reloads. enabledOnly hides the ones the operator switched off. +func (s *readStore) ListBotVerifiers(ctx context.Context, enabledOnly bool, limit int) ([]BotVerifierRow, error) { + limit = clampBotVerificationLimit(limit) + rows, err := s.pool.Query(ctx, ` +SELECT `+botVerifierSelectColumns+botVerifierJoins+` +WHERE NOT $1::boolean OR s.enabled +ORDER BY s.bot_id +LIMIT $2`, enabledOnly, limit) + if err != nil { + return nil, fmt.Errorf("list bot verifiers: %w", err) + } + defer rows.Close() + out := make([]BotVerifierRow, 0, limit) + for rows.Next() { + var item BotVerifierRow + if err := scanBotVerifierRow(rows.Scan, &item); err != nil { + return nil, err + } + out = append(out, item) + } + return out, rows.Err() +} + +// BotVerifier reads one verifier row, enabled or not: the panel needs the disabled +// one too, to render the kill switch. A missing row reports errReadNotFound. +func (s *readStore) BotVerifier(ctx context.Context, botID int64) (BotVerifierRow, error) { + var out BotVerifierRow + row := s.pool.QueryRow(ctx, ` +SELECT `+botVerifierSelectColumns+botVerifierJoins+` +WHERE s.bot_id = $1`, botID) + // The single-row path reuses the list scanner, so one column order serves both: + // a drift between them would silently mis-assign columns. + if err := scanBotVerifierRow(row.Scan, &out); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return out, errReadNotFound + } + return out, fmt.Errorf("get bot verifier: %w", err) + } + return out, nil +} + +// ListVerificationIcons lists the icon catalogue newest first, with the number of +// verifiers each entry is currently configured on -- retiring an entry that +// verifiers still point at is the operator's decision to make knowingly. +func (s *readStore) ListVerificationIcons(ctx context.Context, activeOnly bool, limit int) ([]VerificationIconRow, error) { + limit = clampBotVerificationLimit(limit) + rows, err := s.pool.Query(ctx, ` +SELECT i.id, i.document_id, i.owner_bot_id, + COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS owner_bot_username, + i.name, i.active, + (SELECT count(*) FROM bot_verifier_settings s WHERE s.icon_document_id = i.document_id) AS used_by_verifiers, + i.created_at, i.updated_at +FROM verification_icons i +LEFT JOIN users u ON i.owner_bot_id <> 0 AND u.id = i.owner_bot_id +LEFT JOIN peer_usernames p ON i.owner_bot_id <> 0 + AND p.peer_type = 'user' AND p.peer_id = i.owner_bot_id AND p.editable +WHERE NOT $1::boolean OR i.active +ORDER BY i.id DESC +LIMIT $2`, activeOnly, limit) + if err != nil { + return nil, fmt.Errorf("list verification icons: %w", err) + } + defer rows.Close() + out := make([]VerificationIconRow, 0, limit) + for rows.Next() { + var item VerificationIconRow + if err := rows.Scan( + &item.ID, &item.DocumentID, &item.OwnerBotID, &item.OwnerBotUsername, + &item.Name, &item.Active, &item.UsedByVerifiers, + &item.CreatedAt, &item.UpdatedAt, + ); err != nil { + return nil, err + } + out = append(out, item) + } + return out, rows.Err() +} + +// customVerificationPeerJoins resolves a marked or applied-for peer on both sides +// of the namespace. A third-party mark can sit on a user (bots included) or a +// channel, so both are joined and the row's peer_type decides which contributes. +// Each username side carries `AND editable`, so a collectible username parked in +// peer_usernames is never mistaken for the peer's own handle. +const customVerificationPeerJoins = ` +LEFT JOIN users tu ON %[1]s.peer_type = 'user' AND tu.id = %[1]s.peer_id +LEFT JOIN peer_usernames tup ON %[1]s.peer_type = 'user' + AND tup.peer_type = 'user' AND tup.peer_id = %[1]s.peer_id AND tup.editable +LEFT JOIN channels tc ON %[1]s.peer_type = 'channel' AND tc.id = %[1]s.peer_id +LEFT JOIN peer_usernames tcp ON %[1]s.peer_type = 'channel' + AND tcp.peer_type = 'channel' AND tcp.peer_id = %[1]s.peer_id AND tcp.editable` + +// ListCustomVerifications pages granted marks newest first, keyset by descending +// id. verifierBotID/peerType are exact filters; q matches a mark id, a peer id, or +// a peer username prefix, which is how an operator looks a badge up from a report. +func (s *readStore) ListCustomVerifications( + ctx context.Context, + verifierBotID int64, + peerType, q string, + beforeID int64, + limit int, +) ([]CustomVerificationRow, bool, error) { + limit = clampBotVerificationLimit(limit) + peerType = strings.TrimSpace(peerType) + pattern, queryID := botVerificationSearchTerms(q) + rows, err := s.pool.Query(ctx, ` +SELECT cv.id, cv.verifier_bot_id, + COALESCE(NULLIF(vu.username, ''), vp.username_lower, '') AS verifier_bot_username, + COALESCE(s.company_name, '') AS company_name, + cv.peer_type, cv.peer_id, + CASE cv.peer_type + WHEN 'user' THEN TRIM(BOTH ' ' FROM COALESCE(tu.first_name, '') || ' ' || COALESCE(tu.last_name, '')) + ELSE COALESCE(tc.title, '') + END AS peer_title, + CASE cv.peer_type + WHEN 'user' THEN COALESCE(NULLIF(tu.username, ''), tup.username_lower, '') + ELSE COALESCE(NULLIF(tc.username, ''), tcp.username_lower, '') + END AS peer_username, + cv.icon_document_id, cv.description, cv.created_at, cv.updated_at, cv.version +FROM custom_verifications cv +LEFT JOIN users vu ON vu.id = cv.verifier_bot_id +LEFT JOIN peer_usernames vp ON vp.peer_type = 'user' AND vp.peer_id = cv.verifier_bot_id AND vp.editable +LEFT JOIN bot_verifier_settings s ON s.bot_id = cv.verifier_bot_id`+ + fmt.Sprintf(customVerificationPeerJoins, "cv")+` +WHERE ($1::bigint = 0 OR cv.verifier_bot_id = $1) + AND ($2::text = '' OR cv.peer_type = $2) + AND ($3::bigint = 0 OR cv.id < $3) + AND ($4::text = '' OR ( + ($5::bigint <> 0 AND (cv.id = $5 OR cv.peer_id = $5 OR cv.verifier_bot_id = $5)) + OR lower(COALESCE(tu.username, '')) LIKE $4 + OR lower(COALESCE(tc.username, '')) LIKE $4 + OR lower(COALESCE(tc.title, '')) LIKE $4 + )) +ORDER BY cv.id DESC +LIMIT $6`, verifierBotID, peerType, beforeID, pattern, queryID, limit+1) + if err != nil { + return nil, false, fmt.Errorf("list custom verifications: %w", err) + } + defer rows.Close() + out := make([]CustomVerificationRow, 0, limit+1) + for rows.Next() { + var item CustomVerificationRow + if err := rows.Scan( + &item.ID, &item.VerifierBotID, &item.VerifierBotUsername, &item.CompanyName, + &item.PeerType, &item.PeerID, &item.PeerTitle, &item.PeerUsername, + &item.IconDocumentID, &item.Description, + &item.CreatedAt, &item.UpdatedAt, &item.Version, + ); err != nil { + return nil, false, err + } + out = append(out, item) + } + if err := rows.Err(); err != nil { + return nil, false, err + } + hasMore := len(out) > limit + if hasMore { + out = out[:limit] + } + return out, hasMore, nil +} + +const customVerificationRequestSelectColumns = `r.id, r.verifier_bot_id, + COALESCE(NULLIF(vu.username, ''), vp.username_lower, '') AS verifier_bot_username, + r.applicant_user_id, + COALESCE(NULLIF(au.username, ''), ap.username_lower, '') AS applicant_username, + r.peer_type, r.peer_id, + CASE r.peer_type + WHEN 'user' THEN COALESCE(NULLIF(TRIM(BOTH ' ' FROM COALESCE(tu.first_name, '') || ' ' || COALESCE(tu.last_name, '')), ''), r.peer_title) + ELSE COALESCE(NULLIF(tc.title, ''), r.peer_title) + END AS peer_title, + CASE r.peer_type + WHEN 'user' THEN COALESCE(NULLIF(tu.username, ''), NULLIF(tup.username_lower, ''), r.peer_username) + ELSE COALESCE(NULLIF(tc.username, ''), NULLIF(tcp.username_lower, ''), r.peer_username) + END AS peer_username, + r.reason, r.requested_description, r.status, r.decided_by, r.decision_reason, + r.internal_note, r.correlation_id, + r.created_at, r.updated_at, r.approved_at, r.rejected_at, r.version` + +// customVerificationRequestJoins resolves the verifier bot, the applicant and the +// target peer. The peer title and username fall back to the application's snapshot +// columns: a peer deleted since it applied still has to render as something the +// reviewer recognises. +var customVerificationRequestJoins = ` +FROM custom_verification_requests r +LEFT JOIN users vu ON vu.id = r.verifier_bot_id +LEFT JOIN peer_usernames vp ON vp.peer_type = 'user' AND vp.peer_id = r.verifier_bot_id AND vp.editable +LEFT JOIN users au ON au.id = r.applicant_user_id +LEFT JOIN peer_usernames ap ON ap.peer_type = 'user' AND ap.peer_id = r.applicant_user_id AND ap.editable` + + fmt.Sprintf(customVerificationPeerJoins, "r") + +func scanCustomVerificationRequestRow(scan func(dest ...any) error, item *CustomVerificationRequestRow) error { + // approved_at is NULL until an approval and rejected_at until a rejection; the + // table's CHECK constraints keep each in step with the status. + var approvedAt, rejectedAt *time.Time + if err := scan( + &item.ID, &item.VerifierBotID, &item.VerifierBotUsername, + &item.ApplicantUserID, &item.ApplicantUsername, + &item.PeerType, &item.PeerID, &item.PeerTitle, &item.PeerUsername, + &item.Reason, &item.RequestedDescription, &item.Status, + &item.DecidedBy, &item.DecisionReason, &item.InternalNote, &item.CorrelationID, + &item.CreatedAt, &item.UpdatedAt, &approvedAt, &rejectedAt, &item.Version, + ); err != nil { + return err + } + if approvedAt != nil { + item.ApprovedAt = approvedAt.UTC() + } + if rejectedAt != nil { + item.RejectedAt = rejectedAt.UTC() + } + return nil +} + +// ListCustomVerificationRequests pages the third-party review queue newest first, +// keyset by descending id. status/verifierBotID/peerType are exact filters; q +// matches an application id, a peer id, a verifier id, or a peer/applicant +// username prefix. +func (s *readStore) ListCustomVerificationRequests( + ctx context.Context, + status string, + verifierBotID int64, + peerType, q string, + beforeID int64, + limit int, +) ([]CustomVerificationRequestRow, bool, error) { + limit = clampBotVerificationLimit(limit) + status = strings.TrimSpace(status) + peerType = strings.TrimSpace(peerType) + pattern, queryID := botVerificationSearchTerms(q) + rows, err := s.pool.Query(ctx, ` +SELECT `+customVerificationRequestSelectColumns+customVerificationRequestJoins+` +WHERE ($1::text = '' OR r.status = $1) + AND ($2::bigint = 0 OR r.verifier_bot_id = $2) + AND ($3::text = '' OR r.peer_type = $3) + AND ($4::bigint = 0 OR r.id < $4) + AND ($5::text = '' OR ( + ($6::bigint <> 0 AND (r.id = $6 OR r.peer_id = $6 OR r.verifier_bot_id = $6 OR r.applicant_user_id = $6)) + OR lower(r.peer_username) LIKE $5 + OR lower(r.peer_title) LIKE $5 + OR lower(COALESCE(tu.username, '')) LIKE $5 + OR lower(COALESCE(tc.username, '')) LIKE $5 + OR lower(COALESCE(au.username, '')) LIKE $5 + )) +ORDER BY r.id DESC +LIMIT $7`, status, verifierBotID, peerType, beforeID, pattern, queryID, limit+1) + if err != nil { + return nil, false, fmt.Errorf("list custom verification requests: %w", err) + } + defer rows.Close() + out := make([]CustomVerificationRequestRow, 0, limit+1) + for rows.Next() { + var item CustomVerificationRequestRow + if err := scanCustomVerificationRequestRow(rows.Scan, &item); err != nil { + return nil, false, err + } + out = append(out, item) + } + if err := rows.Err(); err != nil { + return nil, false, err + } + hasMore := len(out) > limit + if hasMore { + out = out[:limit] + } + return out, hasMore, nil +} + +// CustomVerificationRequestDetail returns one application with the verifier behind +// it and whether the mark is live. A missing application reports errReadNotFound so +// the API answers 404 rather than 500. +func (s *readStore) CustomVerificationRequestDetail(ctx context.Context, id int64) (CustomVerificationRequestDetail, error) { + var out CustomVerificationRequestDetail + row := s.pool.QueryRow(ctx, ` +SELECT `+customVerificationRequestSelectColumns+customVerificationRequestJoins+` +WHERE r.id = $1`, id) + if err := scanCustomVerificationRequestRow(row.Scan, &out.Request); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return out, errReadNotFound + } + return out, fmt.Errorf("get custom verification request: %w", err) + } + // The verifier may have been revoked since the application was filed, and the + // application survives that (it references users, not the settings row). An + // absent verifier is reported as a row carrying only its id, so the reviewer can + // still see which bot it was. + verifier, err := s.BotVerifier(ctx, out.Request.VerifierBotID) + switch { + case err == nil: + out.Verifier = verifier + case errors.Is(err, errReadNotFound): + out.Verifier = BotVerifierRow{BotID: out.Request.VerifierBotID} + default: + return out, err + } + if err := s.pool.QueryRow(ctx, ` +SELECT EXISTS ( + SELECT 1 FROM custom_verifications + WHERE verifier_bot_id = $1 AND peer_type = $2 AND peer_id = $3 +)`, out.Request.VerifierBotID, out.Request.PeerType, out.Request.PeerID).Scan(&out.MarkActive); err != nil { + return out, fmt.Errorf("check custom verification mark: %w", err) + } + return out, nil +} + +// CustomVerificationRequestCounts is the queue summary above the list. Every +// modelled status is present, so the panel never has to tell "zero" from "absent", +// and the values are decimal strings for the same exactness reason as the ids. +func (s *readStore) CustomVerificationRequestCounts(ctx context.Context) (map[string]string, error) { + out := map[string]string{} + for _, status := range []domain.CustomVerificationRequestStatus{ + domain.CustomVerificationPending, + domain.CustomVerificationApproved, + domain.CustomVerificationRejected, + domain.CustomVerificationRevoked, + } { + out[string(status)] = "0" + } + rows, err := s.pool.Query(ctx, ` +SELECT status, count(*) FROM custom_verification_requests GROUP BY status`) + if err != nil { + return nil, fmt.Errorf("count custom verification requests: %w", err) + } + defer rows.Close() + for rows.Next() { + var status string + var count int64 + if err := rows.Scan(&status, &count); err != nil { + return nil, err + } + out[status] = strconv.FormatInt(count, 10) + } + return out, rows.Err() +} + +// botVerificationSearchTerms turns an operator query into the LIKE pattern and the +// optional exact id the list predicates use. LIKE metacharacters are escaped: +// usernames legitimately contain '_', so an unescaped search for "crypto_" would +// match "cryptoX" instead of the name that was typed. +func botVerificationSearchTerms(q string) (string, int64) { + query := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(q), "@")) + if query == "" { + return "", 0 + } + pattern := strings.ToLower(escapeLikePattern(query)) + "%" + queryID := int64(0) + if parsed, err := strconv.ParseInt(query, 10, 64); err == nil && parsed > 0 { + queryID = parsed + } + return pattern, queryID +} + +func clampBotVerificationLimit(limit int) int { + if limit <= 0 { + return botVerificationListDefaultLimit + } + if limit > botVerificationListMaxLimit { + return botVerificationListMaxLimit + } + return limit +} diff --git a/cmd/telesrv-admin/readstore_accounts_integration_test.go b/cmd/telesrv-admin/readstore_accounts_integration_test.go new file mode 100644 index 00000000..59db4ee6 --- /dev/null +++ b/cmd/telesrv-admin/readstore_accounts_integration_test.go @@ -0,0 +1,146 @@ +package main + +import ( + "context" + "fmt" + "testing" + "time" +) + +// The Accounts tab is hand-written SQL, so the collectible-username aggregation +// can only be proven against the real schema: the jsonb keys have to match the +// AccountUsername field names for pgx to unmarshal them, the ordering has to match +// the projection order clients see, and the editable slot must not leak into the +// collectible list. Gated on TELESRV_TEST_POSTGRES_DSN like the rest. +func TestReadStoreAccountsCarryCollectibleUsernames(t *testing.T) { + store, pool := verificationReadStore(t) + ctx := context.Background() + suffix := fmt.Sprintf("%d", time.Now().UnixNano()%1_000_000) + userID := 3_600_000_000 + time.Now().UnixNano()%1_000_000 + + editable := "slot" + suffix + // Deliberately out of alphabetical order and with a gap in sort_order, so a + // query that sorted by name or by insertion order would produce a different + // answer than the stored one. + collectibles := []struct { + name string + sortOrder int + active bool + collecting bool + }{ + {name: "zeta" + suffix, sortOrder: 0, active: true, collecting: true}, + {name: "alpha" + suffix, sortOrder: 5, active: false, collecting: true}, + {name: "mid" + suffix, sortOrder: 2, active: true, collecting: true}, + } + + t.Cleanup(func() { + _, _ = pool.Exec(ctx, `DELETE FROM peer_usernames WHERE peer_type='user' AND peer_id=$1`, userID) + _, _ = pool.Exec(ctx, `DELETE FROM collectible_usernames WHERE username_lower LIKE $1`, "%"+suffix) + _, _ = pool.Exec(ctx, `DELETE FROM authorizations WHERE user_id=$1`, userID) + _, _ = pool.Exec(ctx, `DELETE FROM auth_keys WHERE auth_key_id=$1`, userID) + _, _ = 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, '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. + 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) + } + if _, err := pool.Exec(ctx, ` +INSERT INTO authorizations (user_id, auth_key_id, created_at, active_at) +VALUES ($1, $2, now(), now())`, userID, userID); err != nil { + t.Fatalf("seed authorization: %v", err) + } + if _, err := pool.Exec(ctx, ` +INSERT INTO peer_usernames (username_lower, username, peer_type, peer_id, active, editable, sort_order) +VALUES (lower($1), lower($1), 'user', $2, true, true, 0)`, editable, userID); err != nil { + t.Fatalf("seed editable slot: %v", err) + } + for _, item := range collectibles { + var collectibleID int64 + if err := pool.QueryRow(ctx, ` +INSERT INTO collectible_usernames (username, username_lower, status, owner_peer_type, owner_peer_id, + original_owner_peer_type, original_owner_peer_id, purchase_date, currency, amount, created_at, updated_at) +VALUES ($1, lower($1), 'owned', 'user', $2, 'user', $2, now(), 'XTR', 0, now(), now()) +RETURNING id`, item.name, userID).Scan(&collectibleID); err != nil { + t.Fatalf("seed collectible %s: %v", item.name, err) + } + if _, err := pool.Exec(ctx, ` +INSERT INTO peer_usernames (username_lower, username, peer_type, peer_id, active, editable, sort_order, collectible_id) +VALUES (lower($1), lower($1), 'user', $2, $3, false, $4, $5)`, + item.name, userID, item.active, item.sortOrder, collectibleID); err != nil { + t.Fatalf("attach collectible %s: %v", item.name, err) + } + } + + want := []AccountUsername{ + {Username: "zeta" + suffix, Active: true}, + {Username: "mid" + suffix, Active: true}, + {Username: "alpha" + suffix, Active: false}, + } + + detail, err := store.AccountDetail(ctx, userID) + if err != nil { + t.Fatalf("AccountDetail: %v", err) + } + assertCollectibles(t, "AccountDetail", detail.Account, editable, want) + + rows, _, err := store.ListAccounts(ctx, 0, 0, 200) + if err != nil { + t.Fatalf("ListAccounts: %v", err) + } + var listed *AccountRow + for i := range rows { + if rows[i].ID == userID { + listed = &rows[i] + break + } + } + if listed == nil { + t.Fatalf("seeded account %d is absent from the first page of %d accounts", userID, len(rows)) + } + assertCollectibles(t, "ListAccounts", *listed, editable, want) + + // An account holding nothing collectible reports an empty list, not null: the + // panel iterates it unconditionally. + if _, err := pool.Exec(ctx, `DELETE FROM peer_usernames +WHERE peer_type='user' AND peer_id=$1 AND collectible_id IS NOT NULL`, userID); err != nil { + t.Fatalf("drop collectibles: %v", err) + } + bare, err := store.AccountDetail(ctx, userID) + if err != nil { + t.Fatalf("AccountDetail without collectibles: %v", err) + } + if bare.Account.Collectibles == nil || len(bare.Account.Collectibles) != 0 { + t.Fatalf("collectibles without any rows = %#v, want an empty slice", bare.Account.Collectibles) + } +} + +func assertCollectibles(t *testing.T, surface string, row AccountRow, editable string, want []AccountUsername) { + t.Helper() + if row.Username != editable { + t.Fatalf("%s: editable username = %q, want %q", surface, row.Username, editable) + } + if len(row.Collectibles) != len(want) { + t.Fatalf("%s: collectibles = %#v, want %#v", surface, row.Collectibles, want) + } + for i := range want { + if row.Collectibles[i] != want[i] { + t.Fatalf("%s: collectibles = %#v, want %#v", surface, row.Collectibles, want) + } + } + // The editable slot is a different kind of row and must never be repeated in + // the collectible list. + for _, item := range row.Collectibles { + if item.Username == editable { + t.Fatalf("%s: editable slot leaked into the collectible list: %#v", surface, row.Collectibles) + } + } +} diff --git a/cmd/telesrv-admin/readstore_botverification_integration_test.go b/cmd/telesrv-admin/readstore_botverification_integration_test.go new file mode 100644 index 00000000..2a67e10e --- /dev/null +++ b/cmd/telesrv-admin/readstore_botverification_integration_test.go @@ -0,0 +1,571 @@ +package main + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgxpool" +) + +// The third-party verification tables are read with hand-written SQL, so the only +// thing that can prove the column names, the CASE-per-peer-namespace projections +// and the `AND editable` username joins are right is running them against the real +// schema. Gated on TELESRV_TEST_POSTGRES_DSN, like every other integration test in +// the repo, and reusing verificationReadStore for the pool and the migration. + +// botVerificationFixture seeds two verifier bots (one enabled, one switched off), +// a shared and a reserved icon, marks on a user peer and a channel peer, and +// applications in three states. +type botVerificationFixture struct { + verifierBot int64 + disabledBot int64 + applicant int64 + userPeer int64 + channel int64 + sharedIcon int64 + reservedIcon int64 + sharedDoc int64 + reservedDoc int64 + userMark int64 + channelMark int64 + pendingReq int64 + approvedReq int64 + rejectedReq int64 + suffix string +} + +func seedBotVerificationFixture(t *testing.T, pool *pgxpool.Pool) botVerificationFixture { + t.Helper() + ctx := context.Background() + var fx botVerificationFixture + now := time.Now().UTC().Truncate(time.Microsecond) + // Usernames, channel ids and icon document ids are globally unique, so every run + // needs its own suffix: this database may still hold rows another run left. + unique := now.UnixNano() & 0x7fffffff + suffix := strconv.FormatInt(unique, 10) + fx.suffix = suffix + nextChannelID := 1_200_000_000 + unique%100_000_000 + fx.sharedDoc = 7_000_000_000 + unique%1_000_000 + fx.reservedDoc = fx.sharedDoc + 1 + + insertUser := func(first, username string, isBot bool) int64 { + var id int64 + if err := pool.QueryRow(ctx, ` +INSERT INTO users (access_hash, phone, first_name, last_name, username, is_bot) +VALUES ($1, $2, $3, 'Fixture', $4, $5) +RETURNING id`, unique, "71"+strconv.FormatInt(unique, 10), first, username, isBot).Scan(&id); err != nil { + t.Fatalf("insert user %s: %v", first, err) + } + unique++ + return id + } + + fx.verifierBot = insertUser("Verifierbot", "verifierbot"+suffix, true) + fx.disabledBot = insertUser("Disabledbot", "disabledbot"+suffix, true) + fx.applicant = insertUser("Applicant", "bvapplicant"+suffix, false) + fx.userPeer = insertUser("Marked", "markeduser"+suffix, false) + + fx.channel = nextChannelID + if _, err := pool.Exec(ctx, ` +INSERT INTO channels ( + id, access_hash, creator_user_id, title, username, broadcast, megagroup, + participants_count, admins_count, top_message_id, pts, date +) +VALUES ($1, $2, $3, $4, $5, true, false, 1, 1, 1, 1, $6)`, + fx.channel, unique, fx.applicant, "Fixture Marked News", "markednews"+suffix, int32(now.Unix())); err != nil { + t.Fatalf("insert channel: %v", err) + } + unique++ + + insertIcon := func(documentID, ownerBotID int64, name string, active bool) int64 { + var id int64 + if err := pool.QueryRow(ctx, ` +INSERT INTO verification_icons (document_id, owner_bot_id, name, active, created_at, updated_at) +VALUES ($1, $2, $3, $4, $5, $5) RETURNING id`, + documentID, ownerBotID, name, active, now).Scan(&id); err != nil { + t.Fatalf("insert icon %s: %v", name, err) + } + return id + } + fx.sharedIcon = insertIcon(fx.sharedDoc, 0, "shared check "+suffix, true) + // Reserved to the verifier bot and retired, so both the owner join and the + // active filter have a case to answer. + fx.reservedIcon = insertIcon(fx.reservedDoc, fx.verifierBot, "reserved check "+suffix, false) + + insertVerifier := func(botID, documentID int64, company string, enabled, canModify bool) { + if _, err := pool.Exec(ctx, ` +INSERT INTO bot_verifier_settings ( + bot_id, icon_document_id, company_name, default_description, + can_modify_custom_description, enabled, granted_by, grant_reason, + created_at, updated_at, version +) VALUES ($1, $2, $3, 'verified by the fixture', $4, $5, 'alice', 'partner programme', $6, $6, 4)`, + botID, documentID, company, canModify, enabled, now); err != nil { + t.Fatalf("insert verifier %d: %v", botID, err) + } + } + insertVerifier(fx.verifierBot, fx.sharedDoc, "Fixture Trust "+suffix, true, true) + insertVerifier(fx.disabledBot, fx.sharedDoc, "Switched Off "+suffix, false, false) + + insertMark := func(peerType string, peerID int64, description string) int64 { + var id int64 + if err := pool.QueryRow(ctx, ` +INSERT INTO custom_verifications ( + verifier_bot_id, peer_type, peer_id, icon_document_id, description, + granted_by_user_id, created_at, updated_at, version +) VALUES ($1, $2, $3, $4, $5, $1, $6, $6, 2) RETURNING id`, + fx.verifierBot, peerType, peerID, fx.sharedDoc, description, now).Scan(&id); err != nil { + t.Fatalf("insert %s mark: %v", peerType, err) + } + return id + } + fx.userMark = insertMark("user", fx.userPeer, "verified individual") + fx.channelMark = insertMark("channel", fx.channel, "verified outlet") + + insertRequest := func(peerType string, peerID int64, status, reason string) int64 { + var approvedAt, rejectedAt *time.Time + decisionReason := "" + decidedBy := "" + switch status { + case "approved": + approvedAt = &now + decidedBy = "alice" + case "rejected": + rejectedAt = &now + decidedBy = "bob" + decisionReason = reason + } + var id int64 + if err := pool.QueryRow(ctx, ` +INSERT INTO custom_verification_requests ( + verifier_bot_id, applicant_user_id, peer_type, peer_id, peer_title, peer_username, + reason, requested_description, status, decided_by, decision_reason, internal_note, + correlation_id, created_at, updated_at, approved_at, rejected_at, version +) VALUES ( + $1, $2, $3, $4, $5, $6, 'we are the outlet', 'verified partner', $7, $8, $9, + 'operator only', $10, $11, $11, $12, $13, 3 +) RETURNING id`, + fx.verifierBot, fx.applicant, peerType, peerID, + "Snapshot "+peerType, "snapshot"+peerType+suffix, + status, decidedBy, decisionReason, "bvcorr-"+status, + now, approvedAt, rejectedAt, + ).Scan(&id); err != nil { + t.Fatalf("insert %s request: %v", status, err) + } + return id + } + // One live application per (verifier, peer) pair, so the three seeded rows have + // to name three different peers: the partial unique index enforces it. + fx.pendingReq = insertRequest("channel", fx.channel, "pending", "") + fx.approvedReq = insertRequest("user", fx.userPeer, "approved", "") + // Filed against a peer that does not exist, so the live-peer join has a negative + // case and the snapshot fallback is exercised. + fx.rejectedReq = insertRequest("user", fx.userPeer+9_000_000, "rejected", "not an outlet") + + t.Cleanup(func() { + reqIDs := []int64{fx.pendingReq, fx.approvedReq, fx.rejectedReq} + _, _ = pool.Exec(ctx, "DELETE FROM custom_verification_requests WHERE id = ANY($1::bigint[])", reqIDs) + _, _ = pool.Exec(ctx, "DELETE FROM custom_verifications WHERE id = ANY($1::bigint[])", + []int64{fx.userMark, fx.channelMark}) + _, _ = pool.Exec(ctx, "DELETE FROM bot_verifier_settings WHERE bot_id = ANY($1::bigint[])", + []int64{fx.verifierBot, fx.disabledBot}) + _, _ = pool.Exec(ctx, "DELETE FROM verification_icons WHERE id = ANY($1::bigint[])", + []int64{fx.sharedIcon, fx.reservedIcon}) + _, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = $1", fx.channel) + _, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", + []int64{fx.verifierBot, fx.disabledBot, fx.applicant, fx.userPeer}) + }) + return fx +} + +func TestBotVerificationReadStoreVerifiers(t *testing.T) { + store, pool := verificationReadStore(t) + fx := seedBotVerificationFixture(t, pool) + ctx := context.Background() + + rows, err := store.ListBotVerifiers(ctx, false, 200) + if err != nil { + t.Fatalf("list verifiers: %v", err) + } + byID := map[int64]BotVerifierRow{} + for _, row := range rows { + byID[row.BotID] = row + } + verifier, ok := byID[fx.verifierBot] + if !ok { + t.Fatal("enabled verifier missing from the listing") + } + // The bot account is resolved through the join, and the icon's catalogue label + // comes from the entry the document id points at. + if verifier.BotUsername != "verifierbot"+fx.suffix || verifier.IconName != "shared check "+fx.suffix { + t.Fatalf("verifier projection = %+v", verifier) + } + if verifier.BotName == "" || verifier.CompanyName != "Fixture Trust "+fx.suffix { + t.Fatalf("verifier names = %+v", verifier) + } + if !verifier.Enabled || !verifier.CanModifyCustomDescription || verifier.Version != 4 || + verifier.GrantedBy != "alice" || verifier.GrantReason != "partner programme" { + t.Fatalf("verifier settings = %+v", verifier) + } + // Both seeded marks belong to this verifier, and mark_count is what would + // cascade away with a revocation. + if verifier.MarkCount != 2 { + t.Fatalf("mark count = %d, want 2", verifier.MarkCount) + } + if disabled := byID[fx.disabledBot]; disabled.Enabled || disabled.MarkCount != 0 { + t.Fatalf("disabled verifier = %+v", disabled) + } + + // enabled_only hides the switched-off verifier without dropping its row. + enabled, err := store.ListBotVerifiers(ctx, true, 200) + if err != nil { + t.Fatalf("list enabled verifiers: %v", err) + } + for _, row := range enabled { + if !row.Enabled { + t.Fatalf("enabled_only leaked %+v", row) + } + if row.BotID == fx.disabledBot { + t.Fatal("enabled_only returned the switched-off verifier") + } + } + + // The detail read reuses the list scanner, so one column order serves both. + one, err := store.BotVerifier(ctx, fx.verifierBot) + if err != nil { + t.Fatalf("get verifier: %v", err) + } + if one.BotID != fx.verifierBot || one.MarkCount != 2 || one.IconName != verifier.IconName { + t.Fatalf("verifier detail = %+v", one) + } + if _, err := store.BotVerifier(ctx, fx.applicant); err == nil { + t.Fatal("a non-verifier resolved as one") + } + + // The page bound is honoured. + page, err := store.ListBotVerifiers(ctx, false, 1) + if err != nil { + t.Fatalf("bounded list: %v", err) + } + if len(page) != 1 { + t.Fatalf("bounded page len=%d", len(page)) + } +} + +func TestBotVerificationReadStoreIcons(t *testing.T) { + store, pool := verificationReadStore(t) + fx := seedBotVerificationFixture(t, pool) + ctx := context.Background() + + rows, err := store.ListVerificationIcons(ctx, false, 200) + if err != nil { + t.Fatalf("list icons: %v", err) + } + byID := map[int64]VerificationIconRow{} + for _, row := range rows { + byID[row.ID] = row + } + shared, ok := byID[fx.sharedIcon] + if !ok { + t.Fatal("shared icon missing from the catalogue listing") + } + if shared.OwnerBotID != 0 || shared.OwnerBotUsername != "" || !shared.Active { + t.Fatalf("shared icon = %+v, want no owner", shared) + } + // Both seeded verifiers point at the shared document, so retiring it is a + // decision the operator has to make knowingly. + if shared.UsedByVerifiers != 2 { + t.Fatalf("shared icon used_by_verifiers = %d, want 2", shared.UsedByVerifiers) + } + reserved, ok := byID[fx.reservedIcon] + if !ok { + t.Fatal("reserved icon missing from the catalogue listing") + } + if reserved.OwnerBotID != fx.verifierBot || reserved.OwnerBotUsername != "verifierbot"+fx.suffix { + t.Fatalf("reserved icon = %+v, want the owner resolved", reserved) + } + if reserved.Active || reserved.UsedByVerifiers != 0 { + t.Fatalf("reserved icon = %+v", reserved) + } + + // active_only hides the retired entry. + active, err := store.ListVerificationIcons(ctx, true, 200) + if err != nil { + t.Fatalf("list active icons: %v", err) + } + for _, row := range active { + if !row.Active { + t.Fatalf("active_only leaked %+v", row) + } + if row.ID == fx.reservedIcon { + t.Fatal("active_only returned the retired entry") + } + } + // Newest first. + if len(rows) >= 2 && rows[0].ID < rows[1].ID { + t.Fatalf("catalogue is not ordered newest first: %d before %d", rows[0].ID, rows[1].ID) + } +} + +func TestBotVerificationReadStoreMarks(t *testing.T) { + store, pool := verificationReadStore(t) + fx := seedBotVerificationFixture(t, pool) + ctx := context.Background() + + rows, _, err := store.ListCustomVerifications(ctx, fx.verifierBot, "", "", 0, 200) + if err != nil { + t.Fatalf("list marks: %v", err) + } + byID := map[int64]CustomVerificationRow{} + for _, row := range rows { + byID[row.ID] = row + } + // A user peer resolves through users; the verifier's company comes from its + // settings row. + userMark, ok := byID[fx.userMark] + if !ok { + t.Fatal("user mark missing from the listing") + } + if userMark.PeerType != "user" || userMark.PeerID != fx.userPeer || + userMark.PeerUsername != "markeduser"+fx.suffix { + t.Fatalf("user mark peer = %+v", userMark) + } + if userMark.PeerTitle == "" || userMark.VerifierBotUsername != "verifierbot"+fx.suffix || + userMark.CompanyName != "Fixture Trust "+fx.suffix { + t.Fatalf("user mark projection = %+v", userMark) + } + if userMark.IconDocumentID != fx.sharedDoc || userMark.Description != "verified individual" || + userMark.Version != 2 { + t.Fatalf("user mark = %+v", userMark) + } + // A channel peer resolves through channels: the CASE picks the right namespace. + channelMark, ok := byID[fx.channelMark] + if !ok { + t.Fatal("channel mark missing from the listing") + } + if channelMark.PeerType != "channel" || channelMark.PeerTitle != "Fixture Marked News" || + channelMark.PeerUsername != "markednews"+fx.suffix { + t.Fatalf("channel mark peer = %+v", channelMark) + } + + // Filters. + typed, _, err := store.ListCustomVerifications(ctx, fx.verifierBot, "channel", "", 0, 50) + if err != nil { + t.Fatalf("peer_type list: %v", err) + } + for _, row := range typed { + if row.PeerType != "channel" { + t.Fatalf("peer_type filter leaked %+v", row) + } + } + other, _, err := store.ListCustomVerifications(ctx, fx.disabledBot, "", "", 0, 50) + if err != nil { + t.Fatalf("verifier filter list: %v", err) + } + for _, row := range other { + if row.VerifierBotID != fx.disabledBot { + t.Fatalf("verifier filter leaked %+v", row) + } + } + + // q matches a mark id, a peer id, a verifier id and a username or title prefix. + for _, query := range []string{ + strconv.FormatInt(fx.channelMark, 10), + strconv.FormatInt(fx.channel, 10), + strconv.FormatInt(fx.verifierBot, 10), + "markednews" + fx.suffix, + "@markeduser" + fx.suffix, + "fixture marked", + } { + found, _, err := store.ListCustomVerifications(ctx, 0, "", query, 0, 50) + if err != nil { + t.Fatalf("search %q: %v", query, err) + } + if len(found) == 0 { + t.Fatalf("search %q returned nothing", query) + } + } + + // The keyset cursor excludes the row it points at. + after, _, err := store.ListCustomVerifications(ctx, fx.verifierBot, "", "", fx.channelMark, 50) + if err != nil { + t.Fatalf("keyset list: %v", err) + } + for _, row := range after { + if row.ID >= fx.channelMark { + t.Fatalf("keyset page leaked id %d at or after the cursor %d", row.ID, fx.channelMark) + } + } + // The page bound is honoured and reports more. + page, more, err := store.ListCustomVerifications(ctx, fx.verifierBot, "", "", 0, 1) + if err != nil { + t.Fatalf("bounded list: %v", err) + } + if len(page) != 1 || !more { + t.Fatalf("bounded page len=%d hasMore=%v", len(page), more) + } +} + +func TestBotVerificationReadStoreRequestsAndDetail(t *testing.T) { + store, pool := verificationReadStore(t) + fx := seedBotVerificationFixture(t, pool) + ctx := context.Background() + + rows, _, err := store.ListCustomVerificationRequests(ctx, "", fx.verifierBot, "", "", 0, 200) + if err != nil { + t.Fatalf("list requests: %v", err) + } + byID := map[int64]CustomVerificationRequestRow{} + for _, row := range rows { + byID[row.ID] = row + } + pending, ok := byID[fx.pendingReq] + if !ok { + t.Fatal("pending application missing from the queue") + } + if pending.ApplicantUserID != fx.applicant || pending.ApplicantUsername != "bvapplicant"+fx.suffix || + pending.VerifierBotUsername != "verifierbot"+fx.suffix { + t.Fatalf("applicant/verifier projection = %+v", pending) + } + // The peer is read live, not from the snapshot columns: an operator has to see + // the peer as it is now. + if pending.PeerTitle != "Fixture Marked News" || pending.PeerUsername != "markednews"+fx.suffix { + t.Fatalf("live peer projection = %+v, want the channel as it is now", pending) + } + if pending.Status != "pending" || pending.InternalNote != "operator only" || + pending.CorrelationID != "bvcorr-pending" || pending.Version != 3 { + t.Fatalf("operator fields = %+v", pending) + } + if !pending.ApprovedAt.IsZero() || !pending.RejectedAt.IsZero() { + t.Fatalf("timestamps approved=%v rejected=%v, want an undecided application", + pending.ApprovedAt, pending.RejectedAt) + } + if approved := byID[fx.approvedReq]; approved.ApprovedAt.IsZero() || approved.DecidedBy != "alice" { + t.Fatalf("approved application = %+v", approved) + } + if rejected := byID[fx.rejectedReq]; rejected.RejectedAt.IsZero() || rejected.DecisionReason == "" { + t.Fatalf("rejected application = %+v", rejected) + } + // A peer that does not exist falls back to the snapshot the applicant filed + // with, so the row still renders as something the reviewer recognises. + if gone := byID[fx.rejectedReq]; gone.PeerTitle != "Snapshot user" || + gone.PeerUsername != "snapshotuser"+fx.suffix { + t.Fatalf("missing peer = %+v, want the snapshot fallback", gone) + } + + // Filters. + filtered, _, err := store.ListCustomVerificationRequests(ctx, "pending", 0, "", "", 0, 50) + if err != nil { + t.Fatalf("status list: %v", err) + } + for _, row := range filtered { + if row.Status != "pending" { + t.Fatalf("status filter leaked %+v", row) + } + } + typed, _, err := store.ListCustomVerificationRequests(ctx, "", 0, "channel", "", 0, 50) + if err != nil { + t.Fatalf("peer_type list: %v", err) + } + for _, row := range typed { + if row.PeerType != "channel" { + t.Fatalf("peer_type filter leaked %+v", row) + } + } + // q matches an application id, a peer id, the applicant id, and username or + // title prefixes on both the live peer and the snapshot. + for _, query := range []string{ + strconv.FormatInt(fx.pendingReq, 10), + strconv.FormatInt(fx.channel, 10), + strconv.FormatInt(fx.applicant, 10), + "markednews" + fx.suffix, + "snapshotchannel" + fx.suffix, + "@bvapplicant" + fx.suffix, + "snapshot ", + } { + found, _, err := store.ListCustomVerificationRequests(ctx, "", 0, "", query, 0, 50) + if err != nil { + t.Fatalf("search %q: %v", query, err) + } + if len(found) == 0 { + t.Fatalf("search %q returned nothing", query) + } + } + + // The keyset cursor excludes the row it points at, and the bound reports more. + after, _, err := store.ListCustomVerificationRequests(ctx, "", fx.verifierBot, "", "", fx.pendingReq, 50) + if err != nil { + t.Fatalf("keyset list: %v", err) + } + for _, row := range after { + if row.ID >= fx.pendingReq { + t.Fatalf("keyset page leaked id %d at or after the cursor %d", row.ID, fx.pendingReq) + } + } + page, more, err := store.ListCustomVerificationRequests(ctx, "", fx.verifierBot, "", "", 0, 1) + if err != nil { + t.Fatalf("bounded list: %v", err) + } + if len(page) != 1 || !more { + t.Fatalf("bounded page len=%d hasMore=%v", len(page), more) + } + + // The detail read carries the verifier and the live mark state. + detail, err := store.CustomVerificationRequestDetail(ctx, fx.approvedReq) + if err != nil { + t.Fatalf("detail: %v", err) + } + if detail.Request.ID != fx.approvedReq || detail.Verifier.BotID != fx.verifierBot || + detail.Verifier.CompanyName != "Fixture Trust "+fx.suffix { + t.Fatalf("detail = %+v verifier=%+v", detail.Request, detail.Verifier) + } + // The approved application's peer really carries the mark. + if !detail.MarkActive { + t.Fatal("mark_active did not follow the granted mark") + } + // The pending application names the channel, which the fixture also marks, so + // the channel side of the EXISTS probe is covered too. + pendingDetail, err := store.CustomVerificationRequestDetail(ctx, fx.pendingReq) + if err != nil { + t.Fatalf("pending detail: %v", err) + } + if !pendingDetail.MarkActive { + t.Fatal("the channel mark was not seen by the detail read") + } + // The rejected one names a peer nobody marked: mark_active must say so, which is + // what tells "approved" apart from "approved and since stripped". + goneDetail, err := store.CustomVerificationRequestDetail(ctx, fx.rejectedReq) + if err != nil { + t.Fatalf("rejected detail: %v", err) + } + if goneDetail.MarkActive { + t.Fatal("an unmarked peer was reported as carrying a mark") + } + + if _, err := store.CustomVerificationRequestDetail(ctx, 0); err == nil { + t.Fatal("detail of a missing application succeeded") + } +} + +func TestBotVerificationReadStoreCounts(t *testing.T) { + store, pool := verificationReadStore(t) + fx := seedBotVerificationFixture(t, pool) + ctx := context.Background() + + counts, err := store.CustomVerificationRequestCounts(ctx) + if err != nil { + t.Fatalf("counts: %v", err) + } + // Every modelled status is present, so the panel never tells "none" from + // "missing". + for _, status := range []string{"pending", "approved", "rejected", "revoked"} { + if _, ok := counts[status]; !ok { + t.Fatalf("counts %+v missing %q", counts, status) + } + } + if counts["pending"] == "0" || counts["approved"] == "0" || counts["rejected"] == "0" { + t.Fatalf("counts %+v did not see the seeded applications (%d/%d/%d)", + counts, fx.pendingReq, fx.approvedReq, fx.rejectedReq) + } +} diff --git a/cmd/telesrv-admin/readstore_verification_integration_test.go b/cmd/telesrv-admin/readstore_verification_integration_test.go new file mode 100644 index 00000000..4b1e57ae --- /dev/null +++ b/cmd/telesrv-admin/readstore_verification_integration_test.go @@ -0,0 +1,354 @@ +package main + +import ( + "context" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + + "telesrv/internal/domain" + "telesrv/internal/store/postgres" +) + +// The verification review queue is read with hand-written SQL, so the only thing +// that can prove the column names, the array and nullable-timestamp scans, and the +// ownership predicates are right is running them against the real schema. Gated on +// TELESRV_TEST_POSTGRES_DSN, like every other integration test in the repo. + +func verificationReadStore(t *testing.T) (*readStore, *pgxpool.Pool) { + t.Helper() + dsn := os.Getenv("TELESRV_TEST_POSTGRES_DSN") + if dsn == "" { + t.Skip("set TELESRV_TEST_POSTGRES_DSN to run postgres integration test") + } + parsed, err := pgxpool.ParseConfig(dsn) + if err != nil { + t.Fatalf("parse TELESRV_TEST_POSTGRES_DSN: %v", err) + } + if !strings.Contains(strings.ToLower(parsed.ConnConfig.Database), "test") { + t.Fatalf("TELESRV_TEST_POSTGRES_DSN must name a dedicated test database, got %q", parsed.ConnConfig.Database) + } + if err := postgres.Migrate(dsn); err != nil { + t.Fatalf("migrate: %v", err) + } + pool, err := pgxpool.New(context.Background(), dsn) + if err != nil { + t.Fatalf("open pool: %v", err) + } + t.Cleanup(pool.Close) + return newReadStore(pool), pool +} + +// verificationFixture seeds one applicant who owns a bot and administers a public +// channel, plus one unrelated channel nobody controls, and files an application +// against each. It returns the applicant id and the three application ids. +type verificationFixture struct { + applicant int64 + bot int64 + channel int64 + foreign int64 + botApp int64 + channelApp int64 + rejectedApp int64 + searchSuffix string +} + +func seedVerificationFixture(t *testing.T, pool *pgxpool.Pool) verificationFixture { + t.Helper() + ctx := context.Background() + var fx verificationFixture + now := time.Now().UTC().Truncate(time.Microsecond) + // Usernames and channel ids are globally unique, so every run needs its own + // suffix; tests in this package may run against a database another run left + // rows in. + unique := now.UnixNano() & 0x7fffffff + suffix := strconv.FormatInt(unique, 10) + // channels.id carries no sequence: the caller assigns it. + nextChannelID := 1_000_000_000 + unique%100_000_000 + + insertUser := func(name, username string, isBot, verified bool) int64 { + var id int64 + if err := pool.QueryRow(ctx, ` +INSERT INTO users (access_hash, phone, first_name, last_name, username, is_bot, verified) +VALUES ($1, $2, $3, 'Reviewer', $4, $5, $6) +RETURNING id`, unique, "70"+strconv.FormatInt(unique, 10), name, username, isBot, verified).Scan(&id); err != nil { + t.Fatalf("insert user %s: %v", name, err) + } + unique++ + return id + } + insertChannel := func(title, username string, verified bool) int64 { + id := nextChannelID + nextChannelID++ + if _, err := pool.Exec(ctx, ` +INSERT INTO channels ( + id, access_hash, creator_user_id, title, username, broadcast, megagroup, + participants_count, admins_count, top_message_id, pts, date, verified +) +VALUES ($1, $2, $3, $4, $5, true, false, 1, 1, 1, 1, $6, $7)`, + id, unique, fx.applicant, title, username, int32(now.Unix()), verified); err != nil { + t.Fatalf("insert channel %s: %v", title, err) + } + unique++ + return id + } + + fx.applicant = insertUser("Applicant", "applicant"+suffix, false, false) + fx.bot = insertUser("Fixturebot", "fixturebot"+suffix, true, false) + if _, err := pool.Exec(ctx, ` +INSERT INTO bots (bot_user_id, owner_user_id, token_secret) VALUES ($1, $2, 'secret')`, + fx.bot, fx.applicant); err != nil { + t.Fatalf("insert bot: %v", err) + } + fx.channel = insertChannel("Fixture News", "fixturenews"+suffix, true) + fx.foreign = insertChannel("Foreign Channel", "foreignchannel"+suffix, false) + if _, err := pool.Exec(ctx, ` +INSERT INTO user_channel_member_index (user_id, channel_id, status, role, broadcast, public_username) +VALUES ($1, $2, 'active', 'creator', true, true)`, fx.applicant, fx.channel); err != nil { + t.Fatalf("insert member index: %v", err) + } + + insertApplication := func( + targetType string, targetID int64, status, reviewer, reason string, + reviewed bool, + ) int64 { + var reviewedAt *time.Time + if reviewed { + reviewedAt = &now + } + var id int64 + if err := pool.QueryRow(ctx, ` +INSERT INTO verification_applications ( + applicant_user_id, target_type, target_id, target_title, target_username, + category, description, official_website, social_links, press_links, additional_note, + status, reviewer_admin_id, decision_reason, internal_note, correlation_id, + created_at, updated_at, submitted_at, reviewed_at, version +) VALUES ( + $1, $2, $3, $4, $5, + 'media', 'a description long enough to satisfy the domain bar for submission', + 'https://example.test', $6, $7, 'note', + $8, $9, $10, 'operator only', $11, + $12, $12, $12, $13, 3 +) RETURNING id`, + fx.applicant, targetType, targetID, "Snapshot "+targetType, "snapshot"+targetType+suffix, + []string{"https://social.example.test/a"}, + []string{"https://press.example.test/a", "https://press.example.test/b"}, + status, reviewer, reason, "corr-"+status, + now, reviewedAt, + ).Scan(&id); err != nil { + t.Fatalf("insert %s application: %v", status, err) + } + return id + } + + fx.channelApp = insertApplication("channel", fx.channel, "submitted", "", "", false) + fx.botApp = insertApplication("bot", fx.bot, "in_review", "alice", "", false) + // Filed as a user target against an id the applicant is not, so the ownership + // predicate has a negative case to answer. + fx.rejectedApp = insertApplication("user", fx.foreign, "rejected", "bob", "press links are self-published", true) + fx.searchSuffix = suffix + + if _, err := pool.Exec(ctx, ` +INSERT INTO verification_application_events + (application_id, kind, from_status, to_status, actor, reason, note, correlation_id, created_at) +VALUES + ($1, 'submitted', 'draft', 'submitted', '', '', '', 'corr-submitted', $2), + ($1, 'claimed', 'submitted', 'in_review', 'alice', '', 'handover note', 'corr-claimed', $2)`, + fx.channelApp, now); err != nil { + t.Fatalf("insert events: %v", err) + } + + t.Cleanup(func() { + ids := []int64{fx.channelApp, fx.botApp, fx.rejectedApp} + _, _ = pool.Exec(ctx, "DELETE FROM verification_notification_outbox WHERE application_id = ANY($1::bigint[])", ids) + _, _ = pool.Exec(ctx, "DELETE FROM verification_application_events WHERE application_id = ANY($1::bigint[])", ids) + _, _ = pool.Exec(ctx, "DELETE FROM verification_applications WHERE id = ANY($1::bigint[])", ids) + _, _ = pool.Exec(ctx, "DELETE FROM user_channel_member_index WHERE user_id = $1", fx.applicant) + _, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", []int64{fx.channel, fx.foreign}) + _, _ = pool.Exec(ctx, "DELETE FROM bots WHERE bot_user_id = $1", fx.bot) + _, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{fx.applicant, fx.bot}) + }) + return fx +} + +func TestVerificationReadStoreQueue(t *testing.T) { + store, pool := verificationReadStore(t) + fx := seedVerificationFixture(t, pool) + ctx := context.Background() + + rows, hasMore, err := store.ListVerificationApplications(ctx, "", "", "", "", 0, 200) + if err != nil { + t.Fatalf("list: %v", err) + } + byID := map[int64]VerificationApplicationRow{} + for _, row := range rows { + byID[row.ID] = row + } + channelRow, ok := byID[fx.channelApp] + if !ok { + t.Fatalf("submitted application missing from the queue (hasMore=%v)", hasMore) + } + // The applicant is resolved through the join, the arrays survive the scan, and + // the live target badge is read from the peer rather than the snapshot. + if channelRow.ApplicantUserID != fx.applicant || channelRow.ApplicantUsername != "applicant"+fx.searchSuffix { + t.Fatalf("applicant projection = %+v", channelRow) + } + if !strings.Contains(channelRow.ApplicantName, "Applicant") { + t.Fatalf("applicant name = %q", channelRow.ApplicantName) + } + if len(channelRow.SocialLinks) != 1 || len(channelRow.PressLinks) != 2 { + t.Fatalf("link arrays = %+v / %+v", channelRow.SocialLinks, channelRow.PressLinks) + } + if !channelRow.TargetVerified { + t.Fatal("target_verified did not follow the live channel record") + } + if channelRow.InternalNote != "operator only" || channelRow.CorrelationID != "corr-submitted" { + t.Fatalf("operator fields = %+v", channelRow) + } + if channelRow.SubmittedAt.IsZero() || !channelRow.ReviewedAt.IsZero() { + t.Fatalf("timestamps submitted=%v reviewed=%v, want an undecided application", channelRow.SubmittedAt, channelRow.ReviewedAt) + } + if decided := byID[fx.rejectedApp]; decided.ReviewedAt.IsZero() || decided.DecisionReason == "" { + t.Fatalf("decided application = %+v", decided) + } + + // Filters. + filtered, _, err := store.ListVerificationApplications(ctx, "in_review", "", "alice", "", 0, 50) + if err != nil { + t.Fatalf("filtered list: %v", err) + } + for _, row := range filtered { + if row.Status != "in_review" || row.ReviewerAdminID != "alice" { + t.Fatalf("status/reviewer filter leaked %+v", row) + } + } + typed, _, err := store.ListVerificationApplications(ctx, "", "bot", "", "", 0, 50) + if err != nil { + t.Fatalf("target_type list: %v", err) + } + for _, row := range typed { + if row.TargetType != "bot" { + t.Fatalf("target_type filter leaked %+v", row) + } + } + // q matches the application id, the target id and a username prefix. + for _, query := range []string{ + strconv.FormatInt(fx.channelApp, 10), + strconv.FormatInt(fx.channel, 10), + "snapshotchannel" + fx.searchSuffix, + "@applicant" + fx.searchSuffix, + } { + found, _, err := store.ListVerificationApplications(ctx, "", "", "", query, 0, 50) + if err != nil { + t.Fatalf("search %q: %v", query, err) + } + if len(found) == 0 { + t.Fatalf("search %q returned nothing", query) + } + } + // The keyset cursor excludes the row it points at. + after, _, err := store.ListVerificationApplications(ctx, "", "", "", "", fx.channelApp, 50) + if err != nil { + t.Fatalf("keyset list: %v", err) + } + for _, row := range after { + if row.ID >= fx.channelApp { + t.Fatalf("keyset page leaked id %d at or after the cursor %d", row.ID, fx.channelApp) + } + } + // The page bound is honoured and reports more. + page, more, err := store.ListVerificationApplications(ctx, "", "", "", "", 0, 1) + if err != nil { + t.Fatalf("bounded list: %v", err) + } + if len(page) != 1 || !more { + t.Fatalf("bounded page len=%d hasMore=%v", len(page), more) + } +} + +func TestVerificationReadStoreDetailAndOwnership(t *testing.T) { + store, pool := verificationReadStore(t) + fx := seedVerificationFixture(t, pool) + ctx := context.Background() + + detail, err := store.VerificationApplicationDetail(ctx, fx.channelApp) + if err != nil { + t.Fatalf("detail: %v", err) + } + if detail.Application.ID != fx.channelApp || len(detail.Events) != 2 { + t.Fatalf("detail = %+v events=%d", detail.Application, len(detail.Events)) + } + // Newest first, and the operator-only note travels with the event. + if detail.Events[0].Kind != string(domain.VerificationEventClaimed) || + detail.Events[0].Note != "handover note" || detail.Events[0].Actor != "alice" { + t.Fatalf("events = %+v", detail.Events) + } + if !detail.ApplicantControlsTarget { + t.Fatal("channel creator was not recognised as controlling the target") + } + + // A bot the applicant owns. + botDetail, err := store.VerificationApplicationDetail(ctx, fx.botApp) + if err != nil { + t.Fatalf("bot detail: %v", err) + } + if !botDetail.ApplicantControlsTarget { + t.Fatal("bot owner was not recognised as controlling the target") + } + + // A target the applicant has nothing to do with: the application was filed as + // a user target against a foreign channel id, so identity does not match. + foreignDetail, err := store.VerificationApplicationDetail(ctx, fx.rejectedApp) + if err != nil { + t.Fatalf("foreign detail: %v", err) + } + if foreignDetail.ApplicantControlsTarget { + t.Fatal("an unrelated target was reported as controlled") + } + + if _, err := store.VerificationApplicationDetail(ctx, 0); err == nil { + t.Fatal("detail of a missing application succeeded") + } + + // A user target that is the applicant themself is controlled by definition. + controls, err := store.applicantControlsVerificationTarget(ctx, fx.applicant, "user", fx.applicant) + if err != nil || !controls { + t.Fatalf("self target controls=%v err=%v", controls, err) + } + // BotFather is owned by nobody, whatever the bots table says. + controls, err = store.applicantControlsVerificationTarget(ctx, fx.applicant, "bot", domain.BotFatherUserID) + if err != nil || controls { + t.Fatalf("botfather controls=%v err=%v", controls, err) + } + // An unmodelled target type is never controlled. + controls, err = store.applicantControlsVerificationTarget(ctx, fx.applicant, "group", fx.channel) + if err != nil || controls { + t.Fatalf("unmodelled target controls=%v err=%v", controls, err) + } +} + +func TestVerificationReadStoreCounts(t *testing.T) { + store, pool := verificationReadStore(t) + fx := seedVerificationFixture(t, pool) + ctx := context.Background() + + counts, err := store.VerificationStatusCounts(ctx) + if err != nil { + t.Fatalf("counts: %v", err) + } + // Every modelled status is present, so the panel never tells "none" from + // "missing". + for _, status := range []string{"draft", "submitted", "in_review", "approved", "rejected", "cancelled"} { + if _, ok := counts[status]; !ok { + t.Fatalf("counts %+v missing %q", counts, status) + } + } + if counts["submitted"] == "0" || counts["in_review"] == "0" || counts["rejected"] == "0" { + t.Fatalf("counts %+v did not see the seeded applications (%d/%d/%d)", + counts, fx.channelApp, fx.botApp, fx.rejectedApp) + } +} diff --git a/cmd/telesrv-admin/security.go b/cmd/telesrv-admin/security.go new file mode 100644 index 00000000..cf180864 --- /dev/null +++ b/cmd/telesrv-admin/security.go @@ -0,0 +1,210 @@ +package main + +import ( + "context" + "crypto/subtle" + "net/http" + "net/url" + "strings" + "time" +) + +// Panel session authorisation and CSRF. +// +// The panel authenticates with a cookie, which is what makes it a CSRF target: +// a request forged by any other origin arrives with the operator's session +// attached. Two independent checks close that. +// +// 1. Double-submit token. At login the server mints a random token, publishes it +// in a readable cookie (telesrv_admin_csrf) and requires the same value in the +// X-CSRF-Token header of every mutating request. A cross-origin page can make +// the browser *send* the cookie but cannot read it, so it cannot produce the +// header. Double-submit is the right shape here specifically because this +// process keeps no server-side session store: the session lives entirely in a +// signed cookie, so there is nowhere to park a per-session token, and the +// stateless variant is the one that survives a restart and a second replica. +// The token is additionally bound into the signed session claims, so a +// cookie-writing neighbour (a sibling subdomain) cannot supply a matching +// cookie/header pair of its own choosing either. +// +// 2. Origin agreement. When the browser states an Origin, it must be this host. +// That catches a forged request from a page that somehow does hold a token. +// +// Both comparisons are constant time, for the same reason the session MAC is. + +// Panel permission names. They match the strings an operator configures in +// TELESRV_ADMIN_UI_PERMISSIONS and the ones the admin API enforces. +const ( + permissionAll = "*" + permissionVerificationReview = "verification.review" + permissionVerificationRevoke = "verification.revoke" + // Third-party bot verification. Deliberately not implied by the official + // verification rights above: the two are separate mechanisms over separate + // tables, so a session trusted with one queue is not thereby trusted with the + // other. review reads and decides applications; manage appoints verifiers, + // curates the icon catalogue and strips granted marks. + permissionBotVerificationReview = "botverification.review" + permissionBotVerificationManage = "botverification.manage" +) + +type permissionsKey struct{} + +// requireAuthAPI is the gate on every authenticated API route: a valid session, +// and -- for a mutating request -- a valid CSRF token. +func (s *server) requireAuthAPI(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie(sessionCookieName) + if err != nil { + writeAPIError(w, http.StatusUnauthorized, "not authenticated") + return + } + claims, ok := verifySession(s.cfg.SessionKey, cookie.Value, time.Now()) + if !ok { + clearSessionCookie(w) + writeAPIError(w, http.StatusUnauthorized, "not authenticated") + return + } + if !checkMutationSafety(w, r, claims) { + return + } + ctx := context.WithValue(r.Context(), actorKey{}, claims.Actor) + ctx = context.WithValue(ctx, permissionsKey{}, newPanelPermissions(claims.Permissions)) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// requirePermission refuses a session that was not granted the right, before the +// request ever reaches the admin API. The panel is the only caller that can be +// driven by a browser, so the check belongs here as well as upstream: a 403 from +// this process costs no round trip and cannot be confused with a domain failure. +func (s *server) requirePermission(permission string, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !permissionsFromContext(r.Context()).Has(permission) { + writeJSON(w, http.StatusForbidden, map[string]any{ + "error": "permission " + permission + " is required", + "code": "FORBIDDEN", + "permission": permission, + }) + return + } + next.ServeHTTP(w, r) + }) +} + +// checkMutationSafety enforces the CSRF contract on a mutating request. +func checkMutationSafety(w http.ResponseWriter, r *http.Request, claims sessionClaims) bool { + if !mutatingMethod(r.Method) { + return true + } + if !sameOriginRequest(r) { + writeAPIError(w, http.StatusForbidden, "origin is not allowed") + return false + } + cookie, err := r.Cookie(csrfCookieName) + if err != nil || cookie.Value == "" { + writeAPIError(w, http.StatusForbidden, "missing "+csrfCookieName+" cookie; sign in again") + return false + } + header := strings.TrimSpace(r.Header.Get(csrfHeaderName)) + if header == "" { + writeAPIError(w, http.StatusForbidden, "missing "+csrfHeaderName+" header") + return false + } + if subtle.ConstantTimeCompare([]byte(header), []byte(cookie.Value)) != 1 { + writeAPIError(w, http.StatusForbidden, csrfHeaderName+" does not match the "+csrfCookieName+" cookie") + return false + } + // The signed session is the third leg: it pins the pair to the session this + // server issued. A session minted before the token existed carries no CSRF + // claim and is refused, which forces one re-login rather than leaving a + // half-protected session running. + if claims.CSRF == "" || subtle.ConstantTimeCompare([]byte(header), []byte(claims.CSRF)) != 1 { + writeAPIError(w, http.StatusForbidden, "csrf token is not bound to this session; sign in again") + return false + } + return true +} + +// mutatingMethod reports whether the method changes state. GET/HEAD/OPTIONS are +// the safe ones; everything else has to carry a token. +func mutatingMethod(method string) bool { + switch strings.ToUpper(method) { + case http.MethodGet, http.MethodHead, http.MethodOptions: + return false + default: + return true + } +} + +// sameOriginRequest checks the Origin header against the request host. +// +// An absent Origin is accepted: browsers omit it on same-origin requests and +// non-browser callers (curl, tests) never send it, so requiring it would break +// the panel without adding protection the token does not already give. A present +// Origin must be this host -- including the literal "null" a sandboxed or +// privacy-stripped context sends, which is by definition not this host. +// +// This compares against r.Host, so a reverse proxy in front of the panel has to +// preserve it (nginx: proxy_set_header Host $host). +func sameOriginRequest(r *http.Request) bool { + origin := strings.TrimSpace(r.Header.Get("Origin")) + if origin == "" { + return true + } + parsed, err := url.Parse(origin) + if err != nil || parsed.Host == "" { + return false + } + return strings.EqualFold(parsed.Host, r.Host) +} + +// panelPermissions is a resolved session permission set. +type panelPermissions struct { + all bool + names map[string]struct{} + list []string +} + +func newPanelPermissions(permissions []string) panelPermissions { + set := panelPermissions{names: make(map[string]struct{}, len(permissions))} + for _, permission := range permissions { + permission = strings.TrimSpace(permission) + if permission == "" { + continue + } + if _, dup := set.names[permission]; dup { + continue + } + if permission == permissionAll { + set.all = true + } + set.names[permission] = struct{}{} + set.list = append(set.list, permission) + } + return set +} + +// Has reports whether the session was granted the permission. +func (p panelPermissions) Has(permission string) bool { + if p.all { + return true + } + _, ok := p.names[permission] + return ok +} + +// List is what the panel is told about itself, so the UI can hide a section the +// session may not use instead of rendering it into a 403. +func (p panelPermissions) List() []string { + if p.list == nil { + return []string{} + } + return p.list +} + +func permissionsFromContext(ctx context.Context) panelPermissions { + if permissions, ok := ctx.Value(permissionsKey{}).(panelPermissions); ok { + return permissions + } + return panelPermissions{} +} diff --git a/cmd/telesrv-admin/server.go b/cmd/telesrv-admin/server.go index 0185eadc..decdaa6b 100644 --- a/cmd/telesrv-admin/server.go +++ b/cmd/telesrv-admin/server.go @@ -47,7 +47,10 @@ func newServer(cfg uiConfig, read *readStore) (*server, error) { func (s *server) routes() http.Handler { mux := http.NewServeMux() mux.HandleFunc("POST /api/login", s.handleAPILogin) - mux.HandleFunc("POST /api/logout", s.handleAPILogout) + // Logout goes through the same gate as every other mutating route: a forced + // logout is a state change, and an invalid session is cleared by the gate + // itself, so nothing is stranded by protecting it. + mux.Handle("POST /api/logout", s.requireAuthAPI(http.HandlerFunc(s.handleAPILogout))) mux.Handle("GET /api/session", s.requireAuthAPI(http.HandlerFunc(s.handleSession))) mux.Handle("GET /api/accounts", s.requireAuthAPI(http.HandlerFunc(s.handleAccountsAPI))) mux.Handle("GET /api/accounts/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleAccountDetailAPI))) @@ -67,6 +70,10 @@ func (s *server) routes() http.Handler { mux.Handle("GET /api/gifts/{id}/animation", s.requireAuthAPI(http.HandlerFunc(s.handleStarGiftAnimationAPI))) mux.Handle("GET /api/gifts/{id}/collectibles", s.requireAuthAPI(http.HandlerFunc(s.handleStarGiftCollectiblesAPI))) mux.Handle("GET /api/gifts/{id}/collectibles/{kind}/{attribute_id}/animation", s.requireAuthAPI(http.HandlerFunc(s.handleStarGiftCollectibleAnimationAPI))) + mux.Handle("GET /api/collectible-usernames", s.requireAuthAPI(http.HandlerFunc(s.handleCollectibleUsernamesAPI))) + mux.Handle("GET /api/collectible-usernames/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleCollectibleUsernameDetailAPI))) + mux.Handle("GET /api/account-ratings", s.requireAuthAPI(http.HandlerFunc(s.handleAccountRatingsAPI))) + mux.Handle("GET /api/account-ratings/{user_id}", s.requireAuthAPI(http.HandlerFunc(s.handleAccountRatingDetailAPI))) mux.Handle("GET /api/moderation/cases", s.requireAuthAPI(http.HandlerFunc(s.handleModerationCasesAPI))) mux.Handle("GET /api/moderation/cases/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleModerationCaseAPI))) mux.Handle("GET /api/moderation/reports/{id}", s.requireAuthAPI(http.HandlerFunc(s.handleModerationReportAPI))) @@ -99,6 +106,43 @@ func (s *server) routes() http.Handler { mux.Handle("POST /api/actions/set-gift-enabled", s.requireAuthAPI(http.HandlerFunc(s.handleSetStarGiftEnabledAPI))) mux.Handle("POST /api/actions/set-gift-sort-order", s.requireAuthAPI(http.HandlerFunc(s.handleSetStarGiftSortOrderAPI))) mux.Handle("POST /api/actions/give-gift", s.requireAuthAPI(http.HandlerFunc(s.handleGiveGiftAPI))) + 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))) + mux.Handle("POST /api/actions/delete-collectible-username", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteCollectibleUsernameAPI))) + mux.Handle("POST /api/actions/recompute-account-rating", s.requireAuthAPI(http.HandlerFunc(s.handleRecomputeAccountRatingAPI))) + mux.Handle("POST /api/actions/adjust-account-rating", s.requireAuthAPI(http.HandlerFunc(s.handleAdjustAccountRatingAPI))) + // Official platform verification. Every route needs verification.review; + // clearing an existing badge needs verification.revoke on top of it. + mux.Handle("GET /api/verification/applications", s.verificationRead(s.handleVerificationApplicationsAPI)) + mux.Handle("GET /api/verification/applications/{id}", s.verificationRead(s.handleVerificationApplicationDetailAPI)) + mux.Handle("GET /api/verification/counts", s.verificationRead(s.handleVerificationCountsAPI)) + mux.Handle("POST /api/verification/applications/{id}/claim", s.verificationRead(s.handleClaimVerificationAPI)) + mux.Handle("POST /api/verification/applications/{id}/approve", s.verificationRead(s.handleApproveVerificationAPI)) + mux.Handle("POST /api/verification/applications/{id}/reject", s.verificationRead(s.handleRejectVerificationAPI)) + mux.Handle("POST /api/actions/revoke-verification", s.requireAuthAPI( + s.requirePermission(permissionVerificationReview, + s.requirePermission(permissionVerificationRevoke, http.HandlerFunc(s.handleRevokeVerificationAPI))))) + // Third-party bot verification. A separate section from the official + // verification block above -- separate tables, separate rights, separate routes. + // Reads and queue decisions need botverification.review; appointing verifiers, + // curating the icon catalogue and stripping a granted mark need + // botverification.manage. + mux.Handle("GET /api/botverification/verifiers", s.botVerificationRead(s.handleBotVerifiersAPI)) + mux.Handle("GET /api/botverification/icons", s.botVerificationRead(s.handleVerificationIconsAPI)) + mux.Handle("GET /api/botverification/marks", s.botVerificationRead(s.handleCustomVerificationsAPI)) + mux.Handle("GET /api/botverification/requests", s.botVerificationRead(s.handleCustomVerificationRequestsAPI)) + mux.Handle("GET /api/botverification/requests/{id}", s.botVerificationRead(s.handleCustomVerificationRequestDetailAPI)) + mux.Handle("GET /api/botverification/counts", s.botVerificationRead(s.handleCustomVerificationCountsAPI)) + mux.Handle("POST /api/botverification/requests/{id}/approve", s.botVerificationRead(s.handleApproveBotVerificationAPI)) + mux.Handle("POST /api/botverification/requests/{id}/reject", s.botVerificationRead(s.handleRejectBotVerificationAPI)) + mux.Handle("POST /api/botverification/requests/{id}/revoke", s.botVerificationRead(s.handleRevokeBotVerificationAPI)) + mux.Handle("POST /api/actions/grant-bot-verifier", s.botVerificationManage(s.handleGrantBotVerifierAPI)) + mux.Handle("POST /api/actions/set-bot-verifier-enabled", s.botVerificationManage(s.handleSetBotVerifierEnabledAPI)) + mux.Handle("POST /api/actions/revoke-bot-verifier", s.botVerificationManage(s.handleRevokeBotVerifierAPI)) + mux.Handle("POST /api/actions/upsert-verification-icon", s.botVerificationManage(s.handleUpsertVerificationIconAPI)) + mux.Handle("POST /api/actions/set-verification-icon-active", s.botVerificationManage(s.handleSetVerificationIconActiveAPI)) + mux.Handle("POST /api/actions/revoke-custom-verification", s.botVerificationManage(s.handleRevokeCustomVerificationAPI)) mux.HandleFunc("/api/", func(w http.ResponseWriter, _ *http.Request) { writeAPIError(w, http.StatusNotFound, "api route not found") }) @@ -108,23 +152,6 @@ func (s *server) routes() http.Handler { type actorKey struct{} -func (s *server) requireAuthAPI(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie(sessionCookieName) - if err != nil { - writeAPIError(w, http.StatusUnauthorized, "not authenticated") - return - } - claims, ok := verifySession(s.cfg.SessionKey, cookie.Value, time.Now()) - if !ok { - clearSessionCookie(w) - writeAPIError(w, http.StatusUnauthorized, "not authenticated") - return - } - next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), actorKey{}, claims.Actor))) - }) -} - func actorFromContext(ctx context.Context) string { if actor, ok := ctx.Value(actorKey{}).(string); ok && actor != "" { return actor @@ -149,7 +176,18 @@ type loginRequest struct { Secret string `json:"secret"` } +// sessionTTL bounds a signed panel session and the CSRF cookie that goes with it, +// so the two never outlive each other. +const sessionTTL = 12 * time.Hour + func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) { + // Login is the one mutating route without a CSRF token, because no session + // exists yet to bind one to. The Origin check still applies, and the request + // carries the operator credential, which a forging page does not have. + if !sameOriginRequest(r) { + writeAPIError(w, http.StatusForbidden, "origin is not allowed") + return + } var req loginRequest if err := decodeJSON(r, &req); err != nil { writeAPIError(w, http.StatusBadRequest, err.Error()) @@ -159,10 +197,18 @@ func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) { writeAPIError(w, http.StatusUnauthorized, "invalid credential") return } + csrfToken, err := newCSRFToken() + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + permissions := newPanelPermissions(s.cfg.Permissions) value, err := signSession(s.cfg.SessionKey, sessionClaims{ - Actor: "admin", - Exp: time.Now().Add(12 * time.Hour).Unix(), - Nonce: newCommandID("sess"), + Actor: "admin", + Exp: time.Now().Add(sessionTTL).Unix(), + Nonce: newCommandID("sess"), + Permissions: permissions.List(), + CSRF: csrfToken, }) if err != nil { writeAPIError(w, http.StatusInternalServerError, err.Error()) @@ -172,11 +218,16 @@ func (s *server) handleAPILogin(w http.ResponseWriter, r *http.Request) { Name: sessionCookieName, Value: value, Path: "/", - MaxAge: int((12 * time.Hour).Seconds()), + MaxAge: int(sessionTTL.Seconds()), HttpOnly: true, SameSite: http.SameSiteLaxMode, }) - writeJSON(w, http.StatusOK, map[string]any{"actor": "admin"}) + setCSRFCookie(w, csrfToken, sessionTTL) + writeJSON(w, http.StatusOK, map[string]any{ + "actor": "admin", + "permissions": permissions.List(), + "csrf_token": csrfToken, + }) } func (s *server) validSecret(secret string) bool { @@ -194,8 +245,14 @@ func (s *server) handleAPILogout(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"ok": true}) } +// handleSession is what the panel asks on load. It reports the permissions the +// session carries, so the UI can hide a section the operator may not use rather +// than letting them walk into a 403. func (s *server) handleSession(w http.ResponseWriter, r *http.Request) { - writeJSON(w, http.StatusOK, map[string]any{"actor": actorFromContext(r.Context())}) + writeJSON(w, http.StatusOK, map[string]any{ + "actor": actorFromContext(r.Context()), + "permissions": permissionsFromContext(r.Context()).List(), + }) } func (s *server) handleStarGiftsAPI(w http.ResponseWriter, r *http.Request) { @@ -1482,12 +1539,12 @@ func (s *server) handleSetStarGiftSortOrderAPI(w http.ResponseWriter, r *http.Re } type giveGiftAPIRequest struct { - CommandID string `json:"command_id"` - Reason string `json:"reason"` - Confirm bool `json:"confirm"` - SenderUserID int64 `json:"sender_user_id"` - UserID int64 `json:"user_id"` - ChannelID int64 `json:"channel_id"` + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + SenderUserID int64 `json:"sender_user_id"` + UserID int64 `json:"user_id"` + ChannelID int64 `json:"channel_id"` GiftID int64 `json:"gift_id,string"` HideName bool `json:"hide_name"` Message string `json:"message"` @@ -1519,6 +1576,367 @@ func (s *server) handleGiveGiftAPI(w http.ResponseWriter, r *http.Request) { writeCommandResultAPI(w, result, err) } +// flexInt64 decodes an int64 the panel may send either as a JSON number or as a +// decimal string. Ids and nanoton amounts are sent as strings to stay exact past +// 2^53, while a picker-supplied peer id arrives as a plain number; an empty +// string and null both mean "unset", which is how an untouched form field looks. +type flexInt64 int64 + +// Int64 returns the decoded value. +func (v flexInt64) Int64() int64 { return int64(v) } + +func (v *flexInt64) UnmarshalJSON(raw []byte) error { + text, empty := flexScalarText(raw) + if empty { + *v = 0 + return nil + } + parsed, err := strconv.ParseInt(text, 10, 64) + if err != nil { + return fmt.Errorf("invalid integer %s", string(raw)) + } + *v = flexInt64(parsed) + return nil +} + +// flexUnix decodes an optional timestamp as a Unix second count. A date input +// produces an RFC3339 string and a scripted call a plain number, so both are +// accepted; empty means "unset", which the mint command stamps with its clock. +type flexUnix int64 + +// Unix returns the decoded timestamp in seconds, or zero when unset. +func (v flexUnix) Unix() int64 { return int64(v) } + +func (v *flexUnix) UnmarshalJSON(raw []byte) error { + text, empty := flexScalarText(raw) + if empty { + *v = 0 + return nil + } + if parsed, err := strconv.ParseInt(text, 10, 64); err == nil { + *v = flexUnix(parsed) + return nil + } + for _, layout := range []string{time.RFC3339, "2006-01-02"} { + if parsed, err := time.Parse(layout, text); err == nil { + *v = flexUnix(parsed.UTC().Unix()) + return nil + } + } + return fmt.Errorf("invalid timestamp %s", string(raw)) +} + +// flexScalarText unwraps a JSON scalar to its textual form and reports whether +// it carries no value at all (null, empty string, blank). +func flexScalarText(raw []byte) (string, bool) { + text := strings.TrimSpace(string(raw)) + if text == "" || text == "null" { + return "", true + } + if unquoted, err := strconv.Unquote(text); err == nil { + text = strings.TrimSpace(unquoted) + } + if text == "" { + return "", true + } + return text, false +} + +type mintCollectibleUsernameAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + Username string `json:"username"` + OwnerUserID flexInt64 `json:"owner_user_id"` + OwnerChannelID flexInt64 `json:"owner_channel_id"` + Currency string `json:"currency"` + Amount flexInt64 `json:"amount"` + CryptoCurrency string `json:"crypto_currency"` + CryptoAmount flexInt64 `json:"crypto_amount"` + URL string `json:"url"` + PurchaseDate flexUnix `json:"purchase_date"` +} + +func (s *server) handleMintCollectibleUsernameAPI(w http.ResponseWriter, r *http.Request) { + var body mintCollectibleUsernameAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.MintCollectibleUsernameRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "mint-collectible-username"), + Username: body.Username, + OwnerUserID: body.OwnerUserID.Int64(), + OwnerChannelID: body.OwnerChannelID.Int64(), + Currency: body.Currency, + Amount: body.Amount.Int64(), + CryptoCurrency: body.CryptoCurrency, + CryptoAmount: body.CryptoAmount.Int64(), + URL: body.URL, + PurchaseDate: body.PurchaseDate.Unix(), + } + result, err := s.callAdminAPI(r.Context(), "/v1/collectible-usernames/mint", req) + writeCommandResultAPI(w, result, err) +} + +type transferCollectibleUsernameAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + Username string `json:"username"` + ToUserID flexInt64 `json:"to_user_id"` + ToChannelID flexInt64 `json:"to_channel_id"` +} + +func (s *server) handleTransferCollectibleUsernameAPI(w http.ResponseWriter, r *http.Request) { + var body transferCollectibleUsernameAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.TransferCollectibleUsernameRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "transfer-collectible-username"), + Username: body.Username, + ToUserID: body.ToUserID.Int64(), + ToChannelID: body.ToChannelID.Int64(), + } + result, err := s.callAdminAPI(r.Context(), "/v1/collectible-usernames/transfer", req) + writeCommandResultAPI(w, result, err) +} + +type revokeCollectibleUsernameAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + Username string `json:"username"` + Burn bool `json:"burn"` +} + +func (s *server) handleRevokeCollectibleUsernameAPI(w http.ResponseWriter, r *http.Request) { + var body revokeCollectibleUsernameAPIRequest + if !decodeAction(w, r, &body) { + return + } + prefix := "revoke-collectible-username" + if body.Burn { + prefix = "burn-collectible-username" + } + req := admin.RevokeCollectibleUsernameRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, prefix), + Username: body.Username, + Burn: body.Burn, + } + result, err := s.callAdminAPI(r.Context(), "/v1/collectible-usernames/revoke", req) + writeCommandResultAPI(w, result, err) +} + +type deleteCollectibleUsernameAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + Username string `json:"username"` +} + +// handleDeleteCollectibleUsernameAPI erases an asset and its provenance. The +// panel gates it behind the same reason + dry-run + confirm flow as a burn, but +// the outcome differs: the name becomes issuable again from scratch. +func (s *server) handleDeleteCollectibleUsernameAPI(w http.ResponseWriter, r *http.Request) { + var body deleteCollectibleUsernameAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.DeleteCollectibleUsernameRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "delete-collectible-username"), + Username: body.Username, + } + result, err := s.callAdminAPI(r.Context(), "/v1/collectible-usernames/delete", req) + writeCommandResultAPI(w, result, err) +} + +type recomputeAccountRatingAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + UserID flexInt64 `json:"user_id"` +} + +func (s *server) handleRecomputeAccountRatingAPI(w http.ResponseWriter, r *http.Request) { + var body recomputeAccountRatingAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.RecomputeAccountRatingRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "recompute-account-rating"), + UserID: body.UserID.Int64(), + } + result, err := s.callAdminAPI(r.Context(), "/v1/account-ratings/recompute", req) + writeCommandResultAPI(w, result, err) +} + +type adjustAccountRatingAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + UserID flexInt64 `json:"user_id"` + Amount flexInt64 `json:"amount"` +} + +func (s *server) handleAdjustAccountRatingAPI(w http.ResponseWriter, r *http.Request) { + var body adjustAccountRatingAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.AdjustAccountRatingRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "adjust-account-rating"), + UserID: body.UserID.Int64(), + Amount: body.Amount.Int64(), + } + result, err := s.callAdminAPI(r.Context(), "/v1/account-ratings/adjust", req) + writeCommandResultAPI(w, result, err) +} + +// handleCollectibleUsernamesAPI pages the collectible asset table straight from +// PostgreSQL, like every other table view, and echoes the keyset cursor as a +// decimal string so an int64 id survives the round trip through the browser. +func (s *server) handleCollectibleUsernamesAPI(w http.ResponseWriter, r *http.Request) { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + query := r.URL.Query() + status := strings.TrimSpace(query.Get("status")) + switch status { + case "", string(domain.CollectibleUsernameStatusVault), + string(domain.CollectibleUsernameStatusOwned), + string(domain.CollectibleUsernameStatusBurned): + default: + writeAPIError(w, http.StatusBadRequest, "invalid status") + return + } + ownerUserID, err := parseInt64(query.Get("owner_user_id")) + if err != nil || ownerUserID < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid owner_user_id") + return + } + beforeID, err := parseInt64(query.Get("before_id")) + if err != nil || beforeID < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid before_id") + return + } + limit, err := parseInt(query.Get("limit")) + if err != nil || limit < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid limit") + return + } + rows, hasMore, err := s.read.ListCollectibleUsernames(r.Context(), status, ownerUserID, beforeID, query.Get("q"), limit) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + nextBeforeID := "" + if hasMore && len(rows) > 0 { + nextBeforeID = strconv.FormatInt(rows[len(rows)-1].ID, 10) + } + writeJSON(w, http.StatusOK, map[string]any{ + "rows": rows, + "has_more": hasMore, + "next_before_id": nextBeforeID, + }) +} + +func (s *server) handleCollectibleUsernameDetailAPI(w http.ResponseWriter, r *http.Request) { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + id, err := parseInt64(r.PathValue("id")) + if err != nil || id <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid id") + return + } + detail, err := s.read.CollectibleUsernameDetail(r.Context(), id) + if err != nil { + if errors.Is(err, errReadNotFound) { + writeAPIError(w, http.StatusNotFound, "collectible username not found") + return + } + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{ + "asset": detail.Asset, + "transfers": detail.Transfers, + }) +} + +// handleAccountRatingsAPI pages the leaderboard. next_before_id is the last +// user id: the keyset predicate resolves the full (level, stars, user_id) cursor +// from it, so one opaque-looking value is enough to continue the page. +func (s *server) handleAccountRatingsAPI(w http.ResponseWriter, r *http.Request) { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + query := r.URL.Query() + minLevel, err := parseInt(query.Get("min_level")) + if err != nil || minLevel < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid min_level") + return + } + userID, err := parseInt64(query.Get("user_id")) + if err != nil || userID < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid user_id") + return + } + beforeID, err := parseInt64(query.Get("before_id")) + if err != nil || beforeID < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid before_id") + return + } + limit, err := parseInt(query.Get("limit")) + if err != nil || limit < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid limit") + return + } + rows, hasMore, err := s.read.ListAccountRatings(r.Context(), minLevel, userID, beforeID, limit, query.Get("q")) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + nextBeforeID := "" + if hasMore && len(rows) > 0 { + nextBeforeID = strconv.FormatInt(rows[len(rows)-1].UserID, 10) + } + writeJSON(w, http.StatusOK, map[string]any{ + "rows": rows, + "has_more": hasMore, + "next_before_id": nextBeforeID, + }) +} + +func (s *server) handleAccountRatingDetailAPI(w http.ResponseWriter, r *http.Request) { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + userID, err := parseInt64(r.PathValue("user_id")) + if err != nil || userID <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid user_id") + return + } + detail, err := s.read.AccountRatingDetail(r.Context(), userID) + if err != nil { + if errors.Is(err, errReadNotFound) { + writeAPIError(w, http.StatusNotFound, "account rating not found") + return + } + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{ + "rating": detail.Rating, + "events": detail.Events, + }) +} + func (s *server) commandMetaFromAPI(r *http.Request, commandID, reason string, confirm bool, prefix string) admin.CommandMeta { commandID = strings.TrimSpace(commandID) if confirm && strings.HasPrefix(commandID, "dry-") { @@ -1570,6 +1988,42 @@ func (s *server) callAdminAPI(ctx context.Context, apiPath string, payload any) return result, nil } +// callAdminCommand is callAdminAPI with the upstream status preserved. +// +// callAdminAPI deliberately loses it: every caller it has answers 502 for any +// failure. A verification decision needs the distinction, so this variant returns +// the HTTP status alongside the result and lets the handler map it. A status of 0 +// means no HTTP answer was obtained at all. +func (s *server) callAdminCommand(ctx context.Context, apiPath string, payload any) (admin.CommandResult, int, error) { + body, err := json.Marshal(payload) + if err != nil { + return admin.CommandResult{}, 0, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.cfg.AdminAPIURL+apiPath, bytes.NewReader(body)) + if err != nil { + return admin.CommandResult{}, 0, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+s.cfg.AdminAPIToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return admin.CommandResult{}, 0, err + } + defer resp.Body.Close() + raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + var result admin.CommandResult + if err := json.Unmarshal(raw, &result); err != nil { + return result, 0, fmt.Errorf("admin api %s: status=%d body=%s", apiPath, resp.StatusCode, string(raw)) + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + if result.Error == "" { + result.Error = resp.Status + } + return result, resp.StatusCode, errors.New(result.Error) + } + return result, resp.StatusCode, nil +} + func (s *server) callAdminMultipart(ctx context.Context, apiPath string, metadata any, fileName string, data []byte) (admin.CommandResult, error) { var body bytes.Buffer writer := multipart.NewWriter(&body) diff --git a/cmd/telesrv-admin/session.go b/cmd/telesrv-admin/session.go index d66ef12e..86ac8888 100644 --- a/cmd/telesrv-admin/session.go +++ b/cmd/telesrv-admin/session.go @@ -2,6 +2,7 @@ package main import ( "crypto/hmac" + "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" @@ -13,10 +14,29 @@ import ( const sessionCookieName = "telesrv_admin_session" +// csrfCookieName is the double-submit cookie. It is deliberately NOT HttpOnly: +// the panel's own JavaScript has to read it back to echo it in the X-CSRF-Token +// header, which is the whole mechanism. +const csrfCookieName = "telesrv_admin_csrf" + +// csrfHeaderName is the header the panel echoes the cookie in. +const csrfHeaderName = "X-CSRF-Token" + type sessionClaims struct { Actor string `json:"actor"` Exp int64 `json:"exp"` Nonce string `json:"nonce"` + // Permissions is the right set granted to this session, taken from + // TELESRV_ADMIN_UI_PERMISSIONS at login. It travels inside the signed cookie + // rather than being re-read per request, so a session keeps the rights it was + // issued with, and it cannot be edited by the browser: the HMAC covers it. + Permissions []string `json:"permissions,omitempty"` + // CSRF is the double-submit token bound to this session. Binding it into the + // signed claims is what makes the cookie/header pair unforgeable by a sibling + // origin that can only *write* cookies (a subdomain, say): such an attacker + // can set both the cookie and the header to a value they know, but they cannot + // produce a session cookie that agrees with it. + CSRF string `json:"csrf,omitempty"` } func signSession(key []byte, claims sessionClaims) (string, error) { @@ -56,6 +76,28 @@ func verifySession(key []byte, value string, now time.Time) (sessionClaims, bool return claims, true } +// newCSRFToken mints a fresh double-submit token. +func newCSRFToken() (string, error) { + var raw [32]byte + if _, err := rand.Read(raw[:]); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(raw[:]), nil +} + +// setCSRFCookie publishes the token to the browser. +func setCSRFCookie(w http.ResponseWriter, token string, ttl time.Duration) { + http.SetCookie(w, &http.Cookie{ + Name: csrfCookieName, + Value: token, + Path: "/", + MaxAge: int(ttl.Seconds()), + // Readable by the panel's script on purpose; see csrfCookieName. + HttpOnly: false, + SameSite: http.SameSiteLaxMode, + }) +} + func clearSessionCookie(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ Name: sessionCookieName, @@ -65,4 +107,12 @@ func clearSessionCookie(w http.ResponseWriter) { HttpOnly: true, SameSite: http.SameSiteLaxMode, }) + http.SetCookie(w, &http.Cookie{ + Name: csrfCookieName, + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: false, + SameSite: http.SameSiteLaxMode, + }) } diff --git a/cmd/telesrv-admin/session_test.go b/cmd/telesrv-admin/session_test.go index bcebaa9d..1d185f6e 100644 --- a/cmd/telesrv-admin/session_test.go +++ b/cmd/telesrv-admin/session_test.go @@ -216,3 +216,214 @@ func TestSetStarGiftEnabledBFFForwardsExactInt64(t *testing.T) { t.Fatalf("forwarded gift request = %+v", got) } } + +func TestMintCollectibleUsernameBFFForwardsActorAndTolerantScalars(t *testing.T) { + const maxInt64 = int64(9223372036854775807) + var got admin.MintCollectibleUsernameRequest + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/collectible-usernames/mint" || r.Header.Get("Authorization") != "Bearer secret" { + t.Fatalf("upstream request path=%q authorization=%q", r.URL.Path, r.Header.Get("Authorization")) + } + if err := json.NewDecoder(r.Body).Decode(&got); err != nil { + t.Fatal(err) + } + _ = json.NewEncoder(w).Encode(admin.CommandResult{CommandID: got.CommandID, Status: "completed", DryRun: got.DryRun}) + })) + defer upstream.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: upstream.URL, AdminAPIToken: "secret"}} + // The panel sends a picker id as a number, a nanoton amount as a string and an + // RFC3339 purchase date; all three have to survive the hop unchanged. + req := httptest.NewRequest(http.MethodPost, "/api/actions/mint-collectible-username", strings.NewReader(`{ + "reason":"fragment import","confirm":false, + "username":"@Durov","owner_user_id":1001,"currency":"TON", + "amount":"9223372036854775807","crypto_currency":"TON","crypto_amount":"250000000000", + "url":"https://fragment.example/durov","purchase_date":"2026-07-26T00:00:00Z" + }`)) + req = req.WithContext(context.WithValue(req.Context(), actorKey{}, "operator")) + rec := httptest.NewRecorder() + srv.handleMintCollectibleUsernameAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + if got.Actor != "operator" || !got.DryRun || got.CommandID == "" { + t.Fatalf("forwarded command meta = %+v", got.CommandMeta) + } + if got.Username != "@Durov" || got.OwnerUserID != 1001 || got.Amount != maxInt64 || + got.CryptoAmount != 250000000000 || got.PurchaseDate != time.Date(2026, 7, 26, 0, 0, 0, 0, time.UTC).Unix() { + t.Fatalf("forwarded mint request = %+v", got) + } +} + +func TestAdjustAccountRatingBFFForwardsNumericPayload(t *testing.T) { + var got admin.AdjustAccountRatingRequest + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/account-ratings/adjust" { + t.Fatalf("upstream path=%q", r.URL.Path) + } + if err := json.NewDecoder(r.Body).Decode(&got); err != nil { + t.Fatal(err) + } + _ = json.NewEncoder(w).Encode(admin.CommandResult{CommandID: got.CommandID, Status: "completed"}) + })) + defer upstream.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: upstream.URL, AdminAPIToken: "secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/actions/adjust-account-rating", strings.NewReader( + `{"reason":"manual penalty","confirm":true,"user_id":1001,"amount":-2500}`)) + req = req.WithContext(context.WithValue(req.Context(), actorKey{}, "operator")) + rec := httptest.NewRecorder() + srv.handleAdjustAccountRatingAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + if got.Actor != "operator" || got.UserID != 1001 || got.Amount != -2500 || got.DryRun { + t.Fatalf("forwarded adjust request = %+v", got) + } +} + +func TestRevokeCollectibleUsernameBFFRejectsUnknownFields(t *testing.T) { + srv := &server{cfg: uiConfig{AdminAPIURL: "http://127.0.0.1:1", AdminAPIToken: "secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/actions/revoke-collectible-username", strings.NewReader( + `{"reason":"fraud","confirm":true,"username":"durov","burn":true,"actor":"attacker"}`)) + req = req.WithContext(context.WithValue(req.Context(), actorKey{}, "operator")) + rec := httptest.NewRecorder() + srv.handleRevokeCollectibleUsernameAPI(rec, req) + if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "actor") { + t.Fatalf("status=%d body=%s, want 400 rejecting the unknown actor field", rec.Code, rec.Body.String()) + } +} + +func TestCollectibleUsernameAndRatingRowsJSONPreserveInt64AsDecimalStrings(t *testing.T) { + const maxInt64 = int64(9223372036854775807) + raw, err := json.Marshal(CollectibleUsernameRow{ + ID: maxInt64, OwnerPeerID: maxInt64, Amount: maxInt64, CryptoAmount: maxInt64, + OriginalOwnerPeerID: maxInt64, Version: maxInt64, + }) + if err != nil { + t.Fatalf("marshal collectible username row: %v", err) + } + var asset map[string]any + if err := json.Unmarshal(raw, &asset); err != nil { + t.Fatalf("unmarshal collectible username row: %v", err) + } + for _, field := range []string{"ID", "OwnerPeerID", "Amount", "CryptoAmount", "OriginalOwnerPeerID", "Version"} { + if asset[field] != "9223372036854775807" { + t.Fatalf("asset %s = %#v, want exact decimal string", field, asset[field]) + } + } + + raw, err = json.Marshal(AccountRatingRow{ + UserID: maxInt64, Stars: maxInt64, CurrentLevelStars: maxInt64, NextLevelStars: maxInt64, + StarsComponent: maxInt64, ActivityComponent: maxInt64, PenaltyComponent: maxInt64, + ManualComponent: -maxInt64, PendingStars: maxInt64, Version: maxInt64, + }) + if err != nil { + t.Fatalf("marshal account rating row: %v", err) + } + var rating map[string]any + if err := json.Unmarshal(raw, &rating); err != nil { + t.Fatalf("unmarshal account rating row: %v", err) + } + for _, field := range []string{ + "UserID", "Stars", "CurrentLevelStars", "NextLevelStars", + "StarsComponent", "ActivityComponent", "PenaltyComponent", "PendingStars", "Version", + } { + if rating[field] != "9223372036854775807" { + t.Fatalf("rating %s = %#v, want exact decimal string", field, rating[field]) + } + } + if rating["ManualComponent"] != "-9223372036854775807" { + t.Fatalf("rating ManualComponent = %#v, want signed decimal string", rating["ManualComponent"]) + } + + transfer, err := json.Marshal(CollectibleUsernameTransferRow{ + ID: maxInt64, CollectibleID: maxInt64, FromPeerID: maxInt64, ToPeerID: maxInt64, Amount: maxInt64, + }) + if err != nil { + t.Fatalf("marshal transfer row: %v", err) + } + var log map[string]any + if err := json.Unmarshal(transfer, &log); err != nil { + t.Fatalf("unmarshal transfer row: %v", err) + } + for _, field := range []string{"ID", "CollectibleID", "FromPeerID", "ToPeerID", "Amount"} { + if log[field] != "9223372036854775807" { + t.Fatalf("transfer %s = %#v, want exact decimal string", field, log[field]) + } + } +} + +func TestFlexScalarsAcceptNumbersStringsAndBlanks(t *testing.T) { + var body mintCollectibleUsernameAPIRequest + req := httptest.NewRequest(http.MethodPost, "/api/actions/mint-collectible-username", strings.NewReader(`{ + "username":"durov","currency":"XTR","amount":"","owner_user_id":null, + "crypto_amount":"9223372036854775807","purchase_date":"2026-07-26" + }`)) + if err := decodeJSON(req, &body); err != nil { + t.Fatalf("decode mint action: %v", err) + } + if body.Amount.Int64() != 0 || body.OwnerUserID.Int64() != 0 || + body.CryptoAmount.Int64() != 9223372036854775807 || + body.PurchaseDate.Unix() != time.Date(2026, 7, 26, 0, 0, 0, 0, time.UTC).Unix() { + t.Fatalf("decoded mint action = %+v", body) + } + + var rating adjustAccountRatingAPIRequest + numeric := httptest.NewRequest(http.MethodPost, "/api/actions/adjust-account-rating", strings.NewReader( + `{"user_id":1001,"amount":-2500}`)) + if err := decodeJSON(numeric, &rating); err != nil { + t.Fatalf("decode adjust action: %v", err) + } + if rating.UserID.Int64() != 1001 || rating.Amount.Int64() != -2500 { + t.Fatalf("decoded adjust action = %+v", rating) + } + + var broken adjustAccountRatingAPIRequest + invalid := httptest.NewRequest(http.MethodPost, "/api/actions/adjust-account-rating", strings.NewReader( + `{"user_id":"not-a-number"}`)) + if err := decodeJSON(invalid, &broken); err == nil { + t.Fatal("decoded a non-numeric user_id") + } +} + +func TestNewCollectibleAndRatingRoutesRequireSession(t *testing.T) { + srv, err := newServer(uiConfig{SessionKey: []byte("01234567890123456789012345678901")}, nil) + if err != nil { + t.Fatalf("newServer: %v", err) + } + cases := []struct { + method string + path string + }{ + {http.MethodGet, "/api/collectible-usernames"}, + {http.MethodGet, "/api/collectible-usernames/7"}, + {http.MethodGet, "/api/account-ratings"}, + {http.MethodGet, "/api/account-ratings/7"}, + {http.MethodPost, "/api/actions/mint-collectible-username"}, + {http.MethodPost, "/api/actions/transfer-collectible-username"}, + {http.MethodPost, "/api/actions/revoke-collectible-username"}, + {http.MethodPost, "/api/actions/recompute-account-rating"}, + {http.MethodPost, "/api/actions/adjust-account-rating"}, + } + for _, item := range cases { + req := httptest.NewRequest(item.method, item.path, strings.NewReader(`{}`)) + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusUnauthorized { + t.Fatalf("%s %s status=%d, want 401", item.method, item.path, rec.Code) + } + } +} + +func TestEscapeLikePatternKeepsUsernameSearchLiteral(t *testing.T) { + if got := escapeLikePattern("crypto_king"); got != `crypto\_king` { + t.Fatalf("escapeLikePattern underscore = %q", got) + } + if got := escapeLikePattern(`100%_\x`); got != `100\%\_\\x` { + t.Fatalf("escapeLikePattern metacharacters = %q", got) + } + if got := escapeLikePattern(""); got != "" { + t.Fatalf("escapeLikePattern empty = %q", got) + } +} diff --git a/cmd/telesrv-admin/verification.go b/cmd/telesrv-admin/verification.go new file mode 100644 index 00000000..e3074326 --- /dev/null +++ b/cmd/telesrv-admin/verification.go @@ -0,0 +1,262 @@ +package main + +import ( + "errors" + "net/http" + "strconv" + "strings" + + "telesrv/internal/admin" + "telesrv/internal/domain" +) + +// Official platform verification in the panel BFF. +// +// Reads come straight from PostgreSQL, like every other table view, so the queue +// pages without a hop through the admin API and the applicant can be resolved by +// a join. Decisions go the other way -- always through the admin API, so the +// command journal, the status machine and the optimistic lock are enforced in one +// place and a panel action is indistinguishable from an API one in the audit +// trail. + +// verificationRead mounts a route behind a session and the verification.review +// right. +func (s *server) verificationRead(handler http.HandlerFunc) http.Handler { + return s.requireAuthAPI(s.requirePermission(permissionVerificationReview, handler)) +} + +// handleVerificationApplicationsAPI pages the review queue. The filter is +// validated before the read store is consulted: a malformed query is a 400 +// whether or not the database happens to be reachable. +func (s *server) handleVerificationApplicationsAPI(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + status := strings.TrimSpace(query.Get("status")) + if status != "" && !domain.VerificationStatus(status).Valid() { + writeAPIError(w, http.StatusBadRequest, "invalid status") + return + } + targetType := strings.TrimSpace(query.Get("target_type")) + if targetType != "" && !domain.VerificationTargetType(targetType).Valid() { + writeAPIError(w, http.StatusBadRequest, "invalid target_type") + return + } + beforeID, err := parseInt64(query.Get("before_id")) + if err != nil || beforeID < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid before_id") + return + } + limit, err := parseInt(query.Get("limit")) + if err != nil || limit < 0 { + writeAPIError(w, http.StatusBadRequest, "invalid limit") + return + } + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + rows, hasMore, err := s.read.ListVerificationApplications( + r.Context(), status, targetType, strings.TrimSpace(query.Get("reviewer")), query.Get("q"), beforeID, limit, + ) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + nextBeforeID := "" + if hasMore && len(rows) > 0 { + nextBeforeID = strconv.FormatInt(rows[len(rows)-1].ID, 10) + } + writeJSON(w, http.StatusOK, map[string]any{ + "rows": rows, + "has_more": hasMore, + "next_before_id": nextBeforeID, + }) +} + +func (s *server) handleVerificationApplicationDetailAPI(w http.ResponseWriter, r *http.Request) { + id, err := parseInt64(r.PathValue("id")) + if err != nil || id <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid id") + return + } + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + detail, err := s.read.VerificationApplicationDetail(r.Context(), id) + if err != nil { + if errors.Is(err, errReadNotFound) { + writeAPIError(w, http.StatusNotFound, "verification application not found") + return + } + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{ + "application": detail.Application, + "events": detail.Events, + // Both flags describe the target as it is now, not as it was at + // submission: a reviewer has to see that the applicant lost control of the + // peer, or that the badge is already on, before deciding. + "applicant_controls_target": detail.ApplicantControlsTarget, + "target_verified": detail.Application.TargetVerified, + }) +} + +func (s *server) handleVerificationCountsAPI(w http.ResponseWriter, r *http.Request) { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + counts, err := s.read.VerificationStatusCounts(r.Context()) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{"counts": counts}) +} + +// verificationDecisionAPIRequest is the decision payload shared by all three +// per-application actions. version is the optimistic-locking token the reviewer +// read; internal_note is operator-only and is not part of what the applicant is +// told. It is optional everywhere, including on a claim, so one panel form can +// drive all three actions without tripping the strict decoder. +type verificationDecisionAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + Version flexInt64 `json:"version"` + InternalNote string `json:"internal_note"` +} + +func (s *server) handleClaimVerificationAPI(w http.ResponseWriter, r *http.Request) { + id, ok := verificationPathID(w, r) + if !ok { + return + } + var body verificationDecisionAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.ClaimVerificationRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "claim-verification"), + ApplicationID: id, + Version: body.Version.Int64(), + InternalNote: body.InternalNote, + } + result, status, err := s.callAdminCommand(r.Context(), verificationDecisionPath(id, "claim"), req) + writeVerificationResultAPI(w, result, status, err) +} + +func (s *server) handleApproveVerificationAPI(w http.ResponseWriter, r *http.Request) { + id, ok := verificationPathID(w, r) + if !ok { + return + } + var body verificationDecisionAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.ApproveVerificationRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "approve-verification"), + ApplicationID: id, + Version: body.Version.Int64(), + InternalNote: body.InternalNote, + } + result, status, err := s.callAdminCommand(r.Context(), verificationDecisionPath(id, "approve"), req) + writeVerificationResultAPI(w, result, status, err) +} + +func (s *server) handleRejectVerificationAPI(w http.ResponseWriter, r *http.Request) { + id, ok := verificationPathID(w, r) + if !ok { + return + } + var body verificationDecisionAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.RejectVerificationRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "reject-verification"), + ApplicationID: id, + Version: body.Version.Int64(), + InternalNote: body.InternalNote, + } + result, status, err := s.callAdminCommand(r.Context(), verificationDecisionPath(id, "reject"), req) + writeVerificationResultAPI(w, result, status, err) +} + +// revokeVerificationAPIRequest clears a badge. It addresses the target, not an +// application: the approved application stays approved as history. +type revokeVerificationAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + TargetType string `json:"target_type"` + TargetID flexInt64 `json:"target_id"` + InternalNote string `json:"internal_note"` +} + +func (s *server) handleRevokeVerificationAPI(w http.ResponseWriter, r *http.Request) { + var body revokeVerificationAPIRequest + if !decodeAction(w, r, &body) { + return + } + targetType := domain.VerificationTargetType(strings.TrimSpace(body.TargetType)) + if !targetType.Valid() { + writeAPIError(w, http.StatusBadRequest, "invalid target_type") + return + } + if body.TargetID.Int64() <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid target_id") + return + } + req := admin.RevokeVerificationRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "revoke-verification"), + TargetType: targetType, + TargetID: body.TargetID.Int64(), + InternalNote: body.InternalNote, + } + result, status, err := s.callAdminCommand(r.Context(), "/v1/verification/revoke", req) + writeVerificationResultAPI(w, result, status, err) +} + +func verificationPathID(w http.ResponseWriter, r *http.Request) (int64, bool) { + id, err := parseInt64(r.PathValue("id")) + if err != nil || id <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid id") + return 0, false + } + return id, true +} + +func verificationDecisionPath(applicationID int64, action string) string { + return "/v1/verification/applications/" + strconv.FormatInt(applicationID, 10) + "/" + action +} + +// writeVerificationResultAPI relays the admin API's own status to the browser. +// +// The other action handlers flatten every upstream failure into 502, which is +// fine when the only failure mode is "bad request". A verification decision has +// one more: 409 when another reviewer decided first. That has to reach the panel +// as 409, because it is the single case the panel resolves by reloading the +// application rather than by asking the operator to change something. +func writeVerificationResultAPI(w http.ResponseWriter, result admin.CommandResult, status int, err error) { + if err == nil { + writeJSON(w, http.StatusOK, result) + return + } + if result.Status == "" { + result.Status = "failed" + } + if result.Message == "" { + result.Message = "command failed" + } + if result.Error == "" { + result.Error = err.Error() + } + if status < 400 { + // No HTTP answer at all: the admin API was unreachable or unparsable. + status = http.StatusBadGateway + } + writeJSON(w, status, result) +} diff --git a/cmd/telesrv-admin/verification_test.go b/cmd/telesrv-admin/verification_test.go new file mode 100644 index 00000000..3d49c0cf --- /dev/null +++ b/cmd/telesrv-admin/verification_test.go @@ -0,0 +1,690 @@ +package main + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "telesrv/internal/admin" +) + +const testSessionKey = "01234567890123456789012345678901" + +// panelServer builds a BFF whose sessions carry the given permissions. +func panelServer(t *testing.T, permissions ...string) *server { + t.Helper() + srv, err := newServer(uiConfig{ + SessionKey: []byte(testSessionKey), + Password: "letmein", + Permissions: permissions, + }, nil) + if err != nil { + t.Fatalf("newServer: %v", err) + } + return srv +} + +// signIn performs a real login against the routed server and returns the cookies +// plus the CSRF token the panel would echo, so the tests exercise the same pairing +// the browser gets. +func signIn(t *testing.T, srv *server) ([]*http.Cookie, string) { + t.Helper() + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(`{"secret":"letmein"}`)) + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("login status=%d body=%s", rec.Code, rec.Body.String()) + } + var body struct { + Actor string `json:"actor"` + Permissions []string `json:"permissions"` + CSRFToken string `json:"csrf_token"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("decode login: %v", err) + } + if body.CSRFToken == "" { + t.Fatal("login did not mint a csrf token") + } + cookies := rec.Result().Cookies() + var sawCSRFCookie bool + for _, cookie := range cookies { + if cookie.Name != csrfCookieName { + continue + } + sawCSRFCookie = true + if cookie.HttpOnly { + t.Fatal("csrf cookie is HttpOnly; the panel could not read it back") + } + if cookie.Value != body.CSRFToken || cookie.Path != "/" || cookie.SameSite != http.SameSiteLaxMode { + t.Fatalf("csrf cookie=%+v", cookie) + } + } + if !sawCSRFCookie { + t.Fatal("login did not set the csrf cookie") + } + return cookies, body.CSRFToken +} + +func withCookies(req *http.Request, cookies []*http.Cookie) *http.Request { + for _, cookie := range cookies { + req.AddCookie(cookie) + } + return req +} + +func TestPanelSessionReportsPermissions(t *testing.T) { + srv := panelServer(t, permissionVerificationReview) + cookies, _ := signIn(t, srv) + + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies(httptest.NewRequest(http.MethodGet, "/api/session", nil), cookies)) + if rec.Code != http.StatusOK { + t.Fatalf("session status=%d body=%s", rec.Code, rec.Body.String()) + } + var body struct { + Actor string `json:"actor"` + Permissions []string `json:"permissions"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("decode session: %v", err) + } + if body.Actor != "admin" || len(body.Permissions) != 1 || body.Permissions[0] != permissionVerificationReview { + t.Fatalf("session=%+v, want the granted permissions reported to the panel", body) + } +} + +func TestPanelSessionReportsTheWildcardDefault(t *testing.T) { + // The shipped default is the wildcard, so an operator upgrading into the + // permission model keeps every section. + srv := panelServer(t, permissionAll) + cookies, _ := signIn(t, srv) + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies(httptest.NewRequest(http.MethodGet, "/api/session", nil), cookies)) + if !strings.Contains(rec.Body.String(), `"*"`) { + t.Fatalf("session body=%s, want the wildcard reported", rec.Body.String()) + } +} + +func TestMutatingRequestsRequireTheCSRFHeader(t *testing.T) { + srv := panelServer(t, permissionAll) + cookies, token := signIn(t, srv) + const path = "/api/actions/set-verified" + const payload = `{"reason":"official","confirm":false,"user_id":1001,"verified":true}` + + // No header at all. + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies(httptest.NewRequest(http.MethodPost, path, strings.NewReader(payload)), cookies)) + if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), csrfHeaderName) { + t.Fatalf("missing header status=%d body=%s, want 403", rec.Code, rec.Body.String()) + } + + // A header that does not match the cookie. + rec = httptest.NewRecorder() + req := withCookies(httptest.NewRequest(http.MethodPost, path, strings.NewReader(payload)), cookies) + req.Header.Set(csrfHeaderName, token+"-tampered") + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden { + t.Fatalf("mismatched header status=%d body=%s, want 403", rec.Code, rec.Body.String()) + } + + // A matching header from a different session's token: it agrees with the + // cookie the attacker planted but not with the signed session. + otherSrv := panelServer(t, permissionAll) + _, otherToken := signIn(t, otherSrv) + rec = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodPost, path, strings.NewReader(payload)) + for _, cookie := range cookies { + if cookie.Name == sessionCookieName { + req.AddCookie(cookie) + } + } + req.AddCookie(&http.Cookie{Name: csrfCookieName, Value: otherToken}) + req.Header.Set(csrfHeaderName, otherToken) + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), "not bound to this session") { + t.Fatalf("foreign token status=%d body=%s, want 403", rec.Code, rec.Body.String()) + } + + // A session minted before the CSRF token existed is refused rather than left + // half protected. + legacy, err := signSession([]byte(testSessionKey), sessionClaims{ + Actor: "admin", Exp: time.Now().Add(time.Hour).Unix(), Nonce: "n", + Permissions: []string{permissionAll}, + }) + if err != nil { + t.Fatalf("signSession: %v", err) + } + rec = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodPost, path, strings.NewReader(payload)) + req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: legacy}) + req.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "anything"}) + req.Header.Set(csrfHeaderName, "anything") + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden { + t.Fatalf("pre-CSRF session status=%d body=%s, want 403", rec.Code, rec.Body.String()) + } +} + +func TestCSRFProtectionCoversEveryExistingMutatingRoute(t *testing.T) { + srv := panelServer(t, permissionAll) + cookies, _ := signIn(t, srv) + // A representative slice of the routes that predate CSRF: they must all be + // closed, not just the new ones. + for _, path := range []string{ + "/api/logout", + "/api/actions/set-frozen", + "/api/actions/grant-stars", + "/api/actions/delete-bot", + "/api/actions/revoke-collectible-username", + "/api/actions/adjust-account-rating", + "/api/moderation/cases/7/claim", + "/api/verification/applications/7/approve", + "/api/actions/revoke-verification", + } { + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies(httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`)), cookies)) + if rec.Code != http.StatusForbidden { + t.Fatalf("%s status=%d body=%s, want 403 without a csrf header", path, rec.Code, rec.Body.String()) + } + } +} + +func TestReadRequestsDoNotNeedTheCSRFHeader(t *testing.T) { + srv := panelServer(t, permissionAll) + cookies, _ := signIn(t, srv) + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies(httptest.NewRequest(http.MethodGet, "/api/session", nil), cookies)) + if rec.Code != http.StatusOK { + t.Fatalf("GET status=%d body=%s, want a token-free read", rec.Code, rec.Body.String()) + } +} + +func TestForeignOriginIsRefusedEvenWithAValidToken(t *testing.T) { + srv := panelServer(t, permissionAll) + cookies, token := signIn(t, srv) + req := withCookies(httptest.NewRequest(http.MethodPost, "/api/actions/set-verified", strings.NewReader( + `{"reason":"official","confirm":false,"user_id":1001,"verified":true}`)), cookies) + req.Header.Set(csrfHeaderName, token) + req.Header.Set("Origin", "https://evil.example") + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden || !strings.Contains(rec.Body.String(), "origin") { + t.Fatalf("foreign origin status=%d body=%s, want 403", rec.Code, rec.Body.String()) + } + + // The panel's own origin is accepted. + if !sameOriginRequest(originRequest("https://panel.example", "panel.example")) { + t.Fatal("same origin refused") + } + // A missing Origin is accepted: browsers omit it and non-browser callers never + // send it, and the token check still applies. + if !sameOriginRequest(originRequest("", "panel.example")) { + t.Fatal("absent origin refused") + } + // An opaque origin is not this host. + if sameOriginRequest(originRequest("null", "panel.example")) { + t.Fatal("opaque origin accepted") + } + if sameOriginRequest(originRequest("not a url", "panel.example")) { + t.Fatal("unparsable origin accepted") + } +} + +func originRequest(origin, host string) *http.Request { + req := httptest.NewRequest(http.MethodPost, "/api/actions/set-verified", nil) + req.Host = host + if origin != "" { + req.Header.Set("Origin", origin) + } + return req +} + +func TestLoginRefusesAForeignOrigin(t *testing.T) { + srv := panelServer(t, permissionAll) + req := httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(`{"secret":"letmein"}`)) + req.Header.Set("Origin", "https://evil.example") + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden { + t.Fatalf("cross-origin login status=%d body=%s, want 403", rec.Code, rec.Body.String()) + } +} + +func TestVerificationRoutesRefuseASessionWithoutTheReviewRight(t *testing.T) { + srv := panelServer(t, "gifts.import") + cookies, token := signIn(t, srv) + cases := []struct { + method string + path string + body string + }{ + {http.MethodGet, "/api/verification/applications", ""}, + {http.MethodGet, "/api/verification/applications/7", ""}, + {http.MethodGet, "/api/verification/counts", ""}, + {http.MethodPost, "/api/verification/applications/7/claim", `{}`}, + {http.MethodPost, "/api/verification/applications/7/approve", `{}`}, + {http.MethodPost, "/api/verification/applications/7/reject", `{}`}, + {http.MethodPost, "/api/actions/revoke-verification", `{}`}, + } + for _, item := range cases { + var req *http.Request + if item.body == "" { + req = httptest.NewRequest(item.method, item.path, nil) + } else { + req = httptest.NewRequest(item.method, item.path, strings.NewReader(item.body)) + req.Header.Set(csrfHeaderName, token) + } + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies(req, cookies)) + if rec.Code != http.StatusForbidden { + t.Fatalf("%s %s status=%d body=%s, want 403", item.method, item.path, rec.Code, rec.Body.String()) + } + var body map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("decode 403 body: %v", err) + } + if body["code"] != "FORBIDDEN" || body["permission"] != permissionVerificationReview { + t.Fatalf("%s 403 body=%+v, want the missing permission named", item.path, body) + } + } +} + +func TestRevokeVerificationNeedsTheRevokeRightOnTopOfReview(t *testing.T) { + srv := panelServer(t, permissionVerificationReview) + cookies, token := signIn(t, srv) + req := withCookies(httptest.NewRequest(http.MethodPost, "/api/actions/revoke-verification", strings.NewReader( + `{"reason":"impersonation","confirm":true,"target_type":"channel","target_id":5005}`)), cookies) + req.Header.Set(csrfHeaderName, token) + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusForbidden { + t.Fatalf("status=%d body=%s, want 403", rec.Code, rec.Body.String()) + } + var body map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatalf("decode 403 body: %v", err) + } + if body["permission"] != permissionVerificationRevoke { + t.Fatalf("403 body=%+v, want verification.revoke named", body) + } +} + +func TestVerificationRoutesRequireASession(t *testing.T) { + srv := panelServer(t, permissionAll) + cases := []struct { + method string + path string + }{ + {http.MethodGet, "/api/verification/applications"}, + {http.MethodGet, "/api/verification/applications/7"}, + {http.MethodGet, "/api/verification/counts"}, + {http.MethodPost, "/api/verification/applications/7/claim"}, + {http.MethodPost, "/api/verification/applications/7/approve"}, + {http.MethodPost, "/api/verification/applications/7/reject"}, + {http.MethodPost, "/api/actions/revoke-verification"}, + } + for _, item := range cases { + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, httptest.NewRequest(item.method, item.path, strings.NewReader(`{}`))) + if rec.Code != http.StatusUnauthorized { + t.Fatalf("%s %s status=%d, want 401", item.method, item.path, rec.Code) + } + } +} + +// verificationUpstream stands in for the admin API and records what the BFF sent. +type verificationUpstream struct { + path string + raw []byte + status int + body any +} + +func (u *verificationUpstream) handler(t *testing.T) http.HandlerFunc { + t.Helper() + return func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer api-secret" { + t.Fatalf("upstream authorization=%q", r.Header.Get("Authorization")) + } + u.path = r.URL.Path + defer r.Body.Close() + raw, err := io.ReadAll(r.Body) + if err != nil { + t.Fatalf("read upstream body: %v", err) + } + u.raw = raw + status := u.status + if status == 0 { + status = http.StatusOK + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(u.body) + } +} + +// requestWithActor stands in for the session middleware, which is what puts the +// signed-in operator into the request context. +func requestWithActor(r *http.Request, actor string) *http.Request { + return r.WithContext(context.WithValue(r.Context(), actorKey{}, actor)) +} + +func TestApproveVerificationBFFForwardsActorVersionAndNote(t *testing.T) { + upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed"}} + api := httptest.NewServer(upstream.handler(t)) + defer api.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/verification/applications/77/approve", strings.NewReader(`{ + "reason":"press coverage verified","confirm":true,"version":"9223372036854775807", + "internal_note":"contact came through the press office" + }`)) + req.SetPathValue("id", "77") + req = requestWithActor(req, "operator") + rec := httptest.NewRecorder() + srv.handleApproveVerificationAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + if upstream.path != "/v1/verification/applications/77/approve" { + t.Fatalf("upstream path=%q", upstream.path) + } + var got admin.ApproveVerificationRequest + if err := json.Unmarshal(upstream.raw, &got); err != nil { + t.Fatalf("decode forwarded approval: %v (%s)", err, upstream.raw) + } + if got.Actor != "operator" { + t.Fatalf("actor=%q, want the signed-in operator", got.Actor) + } + if got.ApplicationID != 77 || got.Version != 9223372036854775807 { + t.Fatalf("forwarded approval=%+v, want the exact int64 version", got) + } + if got.InternalNote != "contact came through the press office" || got.DryRun { + t.Fatalf("forwarded approval=%+v", got) + } + if got.CommandID == "" { + t.Fatal("no command id was minted for the idempotency key") + } +} + +func TestClaimVerificationBFFDefaultsToADryRun(t *testing.T) { + upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed", DryRun: true}} + api := httptest.NewServer(upstream.handler(t)) + defer api.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/verification/applications/77/claim", strings.NewReader( + `{"reason":"queue sweep","confirm":false,"version":3}`)) + req.SetPathValue("id", "77") + req = requestWithActor(req, "operator") + rec := httptest.NewRecorder() + srv.handleClaimVerificationAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + var got admin.ClaimVerificationRequest + if err := json.Unmarshal(upstream.raw, &got); err != nil { + t.Fatalf("decode forwarded claim: %v", err) + } + // confirm=false is a rehearsal: nothing may be written until the operator + // confirms. + if !got.DryRun || got.Version != 3 || got.ApplicationID != 77 { + t.Fatalf("forwarded claim=%+v", got) + } +} + +func TestRevokeVerificationBFFForwardsTargetAndRejectsBadShapes(t *testing.T) { + upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed"}} + api := httptest.NewServer(upstream.handler(t)) + defer api.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}} + req := requestWithActor(httptest.NewRequest(http.MethodPost, "/api/actions/revoke-verification", strings.NewReader(`{ + "reason":"impersonation confirmed","confirm":true,"target_type":"channel", + "target_id":"9223372036854775807","internal_note":"legal asked for it" + }`)), "operator") + rec := httptest.NewRecorder() + srv.handleRevokeVerificationAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + if upstream.path != "/v1/verification/revoke" { + t.Fatalf("upstream path=%q", upstream.path) + } + var got admin.RevokeVerificationRequest + if err := json.Unmarshal(upstream.raw, &got); err != nil { + t.Fatalf("decode forwarded revocation: %v", err) + } + if got.TargetID != 9223372036854775807 || got.TargetType != "channel" || + got.Actor != "operator" || got.InternalNote != "legal asked for it" || got.DryRun { + t.Fatalf("forwarded revocation=%+v", got) + } + + for _, payload := range []string{ + `{"reason":"x","confirm":true,"target_type":"group","target_id":5}`, + `{"reason":"x","confirm":true,"target_type":"channel","target_id":0}`, + } { + rec := httptest.NewRecorder() + srv.handleRevokeVerificationAPI(rec, requestWithActor( + httptest.NewRequest(http.MethodPost, "/api/actions/revoke-verification", strings.NewReader(payload)), "operator")) + if rec.Code != http.StatusBadRequest { + t.Fatalf("payload %s status=%d body=%s, want 400", payload, rec.Code, rec.Body.String()) + } + } +} + +func TestVerificationDecisionRejectsUnknownFields(t *testing.T) { + srv := &server{cfg: uiConfig{AdminAPIURL: "http://127.0.0.1:1", AdminAPIToken: "api-secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/verification/applications/77/approve", strings.NewReader( + `{"reason":"ok","confirm":true,"version":3,"actor":"attacker"}`)) + req.SetPathValue("id", "77") + req = requestWithActor(req, "operator") + rec := httptest.NewRecorder() + srv.handleApproveVerificationAPI(rec, req) + if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), "actor") { + t.Fatalf("status=%d body=%s, want 400 rejecting the injected actor", rec.Code, rec.Body.String()) + } +} + +func TestVerificationVersionConflictReachesThePanelAs409(t *testing.T) { + upstream := &verificationUpstream{ + status: http.StatusConflict, + body: admin.CommandResult{ + CommandID: "c1", Status: "failed", + Error: admin.CodeVerificationConflict + ": verification application changed concurrently", + Message: "another reviewer changed this application first; reload it and decide again", + }, + } + api := httptest.NewServer(upstream.handler(t)) + defer api.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/verification/applications/77/approve", strings.NewReader( + `{"reason":"ok","confirm":true,"version":3}`)) + req.SetPathValue("id", "77") + req = requestWithActor(req, "operator") + rec := httptest.NewRecorder() + srv.handleApproveVerificationAPI(rec, req) + // A flattened 502 would hide the one failure the panel resolves by reloading. + if rec.Code != http.StatusConflict { + t.Fatalf("status=%d body=%s, want 409", rec.Code, rec.Body.String()) + } + var result admin.CommandResult + if err := json.Unmarshal(rec.Body.Bytes(), &result); err != nil { + t.Fatalf("decode conflict: %v", err) + } + if !strings.Contains(result.Error, admin.CodeVerificationConflict) || !strings.Contains(result.Message, "reload") { + t.Fatalf("relayed result=%+v", result) + } +} + +func TestVerificationUnreachableAdminAPIIsABadGateway(t *testing.T) { + srv := &server{cfg: uiConfig{AdminAPIURL: "http://127.0.0.1:1", AdminAPIToken: "api-secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/verification/applications/77/reject", strings.NewReader( + `{"reason":"press links are self-published","confirm":true,"version":3}`)) + req.SetPathValue("id", "77") + req = requestWithActor(req, "operator") + rec := httptest.NewRecorder() + srv.handleRejectVerificationAPI(rec, req) + if rec.Code != http.StatusBadGateway { + t.Fatalf("status=%d body=%s, want 502", rec.Code, rec.Body.String()) + } +} + +func TestVerificationRowsJSONPreserveInt64AsDecimalStrings(t *testing.T) { + const maxInt64 = int64(9223372036854775807) + raw, err := json.Marshal(VerificationApplicationRow{ + ID: maxInt64, ApplicantUserID: maxInt64, TargetID: maxInt64, Version: maxInt64, + }) + if err != nil { + t.Fatalf("marshal verification row: %v", err) + } + var application map[string]any + if err := json.Unmarshal(raw, &application); err != nil { + t.Fatalf("unmarshal verification row: %v", err) + } + for _, field := range []string{"ID", "ApplicantUserID", "TargetID", "Version"} { + if application[field] != "9223372036854775807" { + t.Fatalf("application %s = %#v, want an exact decimal string", field, application[field]) + } + } + + raw, err = json.Marshal(VerificationEventRow{ID: maxInt64}) + if err != nil { + t.Fatalf("marshal verification event row: %v", err) + } + var event map[string]any + if err := json.Unmarshal(raw, &event); err != nil { + t.Fatalf("unmarshal verification event row: %v", err) + } + if event["ID"] != "9223372036854775807" { + t.Fatalf("event ID = %#v, want an exact decimal string", event["ID"]) + } +} + +func TestVerificationQueryValidationRejectsUnmodelledFilters(t *testing.T) { + srv := panelServer(t, permissionVerificationReview) + cookies, _ := signIn(t, srv) + for _, query := range []string{"?status=pending", "?target_type=group", "?before_id=-1", "?limit=abc"} { + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies( + httptest.NewRequest(http.MethodGet, "/api/verification/applications"+query, nil), cookies)) + // The read store is absent in this fixture, so a rejected filter is a 400 + // and an accepted one would be a 503: either way the validation is proven. + if rec.Code != http.StatusBadRequest { + t.Fatalf("%s status=%d body=%s, want 400", query, rec.Code, rec.Body.String()) + } + } + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, withCookies( + httptest.NewRequest(http.MethodGet, "/api/verification/applications?status=submitted&target_type=channel", nil), cookies)) + if rec.Code != http.StatusServiceUnavailable { + t.Fatalf("valid filter status=%d body=%s, want the read store to be reached", rec.Code, rec.Body.String()) + } +} + +func TestPanelPermissionsWildcardAndMembership(t *testing.T) { + all := newPanelPermissions([]string{permissionAll}) + if !all.Has(permissionVerificationReview) || !all.Has(permissionVerificationRevoke) { + t.Fatal("wildcard session refused a permission") + } + bounded := newPanelPermissions([]string{" verification.review ", "", "verification.review"}) + if !bounded.Has(permissionVerificationReview) || bounded.Has(permissionVerificationRevoke) { + t.Fatalf("bounded session = %+v", bounded.List()) + } + if len(bounded.List()) != 1 { + t.Fatalf("bounded list=%+v, want the duplicate collapsed", bounded.List()) + } + if got := newPanelPermissions(nil).List(); got == nil || len(got) != 0 { + t.Fatalf("empty list=%#v, want an empty array rather than null", got) + } +} + +// The CSRF gate must let a correctly-tokened request through -- including on the +// routes that predate it -- or the panel is simply broken rather than protected. +func TestExistingMutatingRoutesStillWorkWithAValidToken(t *testing.T) { + upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed", DryRun: true}} + api := httptest.NewServer(upstream.handler(t)) + defer api.Close() + + srv := panelServer(t, permissionAll) + srv.cfg.AdminAPIURL = api.URL + srv.cfg.AdminAPIToken = "api-secret" + cookies, token := signIn(t, srv) + + req := withCookies(httptest.NewRequest(http.MethodPost, "/api/actions/set-verified", strings.NewReader( + `{"reason":"official","confirm":false,"user_id":1001,"verified":true}`)), cookies) + req.Header.Set(csrfHeaderName, token) + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("tokened legacy action status=%d body=%s", rec.Code, rec.Body.String()) + } + if upstream.path != "/v1/accounts/set-verified" { + t.Fatalf("upstream path=%q", upstream.path) + } + + // And logout, which is now behind the same gate. + req = withCookies(httptest.NewRequest(http.MethodPost, "/api/logout", nil), cookies) + req.Header.Set(csrfHeaderName, token) + rec = httptest.NewRecorder() + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("tokened logout status=%d body=%s", rec.Code, rec.Body.String()) + } + // Both cookies are cleared, so the browser cannot keep replaying either half. + cleared := map[string]bool{} + for _, cookie := range rec.Result().Cookies() { + if cookie.MaxAge < 0 { + cleared[cookie.Name] = true + } + } + if !cleared[sessionCookieName] || !cleared[csrfCookieName] { + t.Fatalf("logout cleared=%+v, want both cookies expired", cleared) + } +} + +// The panel drives claim, approve and reject from one form, so a claim carrying an +// internal note must not be rejected by the strict decoder. +func TestClaimVerificationAcceptsAnOptionalInternalNote(t *testing.T) { + upstream := &verificationUpstream{body: admin.CommandResult{CommandID: "c1", Status: "completed"}} + api := httptest.NewServer(upstream.handler(t)) + defer api.Close() + + srv := &server{cfg: uiConfig{AdminAPIURL: api.URL, AdminAPIToken: "api-secret"}} + req := httptest.NewRequest(http.MethodPost, "/api/verification/applications/77/claim", strings.NewReader( + `{"reason":"queue sweep","confirm":true,"version":3,"internal_note":"waiting on legal"}`)) + req.SetPathValue("id", "77") + req = requestWithActor(req, "operator") + rec := httptest.NewRecorder() + srv.handleClaimVerificationAPI(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + var got admin.ClaimVerificationRequest + if err := json.Unmarshal(upstream.raw, &got); err != nil { + t.Fatalf("decode forwarded claim: %v", err) + } + if got.InternalNote != "waiting on legal" { + t.Fatalf("forwarded claim=%+v", got) + } +} + +func TestMutatingMethodClassification(t *testing.T) { + for _, method := range []string{http.MethodGet, http.MethodHead, http.MethodOptions, "get"} { + if mutatingMethod(method) { + t.Fatalf("%s classified as mutating", method) + } + } + for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete} { + if !mutatingMethod(method) { + t.Fatalf("%s classified as safe", method) + } + } +} diff --git a/cmd/telesrv-admin/web/dist/assets/index-D5Lc7N2D.css b/cmd/telesrv-admin/web/dist/assets/index-D5Lc7N2D.css deleted file mode 100644 index 87548202..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-D5Lc7N2D.css +++ /dev/null @@ -1 +0,0 @@ -:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#eef1f5;--bg-accent:#e7ecf1;--panel:#fff;--panel-subtle:#f5f8fb;--panel-strong:#eef2f6;--surface-soft:#f2f7f6;--overlay:#18222f6b;--topbar-bg:#ffffffdb;--line:#e5eaf0;--line-strong:#d3dce4;--heading:#253040;--text:#333f4d;--text-soft:#45525f;--muted:#6d7885;--muted-2:#9aa4b1;--brand:#1f7d6f;--brand-strong:#196155;--brand-2:#3a6cae;--brand-tint:#e8f4f0;--brand-tint-border:#c8e2db;--brand-tint-text:#235d53;--good:#1f8a57;--good-tint:#eaf6ef;--good-border:#c1e1cf;--warn:#a86a12;--warn-tint:#fcf4e4;--warn-border:#e7d09e;--danger:#c0392b;--danger-tint:#fcefec;--danger-border:#eecac3;--danger-text:#8f2f27;--purple:#6a4fa3;--purple-tint:#f4effb;--purple-border:#dcd0f0;--purple-text:#5a4590;--input-bg:#fff;--btn-bg:#fff;--btn-text:#29323d;--btn-hover:#f4f7fa;--switch-track:#c8d0d6;--code-bg:#1b2733;--code-text:#d6e3ef;--code-border:#2b3a49;--sidebar:#1c2530;--sidebar-soft:#26313d;--sidebar-line:#313c4a;--sidebar-row:#232d38;--sidebar-text:#dbe3ec;--sidebar-muted:#8b98a8;--sidebar-faint:#7c8a9a;--sidebar-heading:#fff;--focus:#1f7d6f29;--shadow:0 12px 34px #1827381a;--shadow-sm:0 2px 10px #1827380d;--shadow-brand:0 8px 22px #1f7d6f38;--radius-xs:8px;--radius-sm:9px;--radius:11px;--radius-lg:14px}[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:#0f141a;--bg-accent:#131a22;--panel:#171f28;--panel-subtle:#1c2530;--panel-strong:#212c38;--surface-soft:#1a232d;--overlay:#05080c9e;--topbar-bg:#151c24db;--line:#29333f;--line-strong:#38434f;--heading:#eef3f8;--text:#d5dde6;--text-soft:#c2ccd6;--muted:#98a4b1;--muted-2:#6d7885;--brand:#37a596;--brand-strong:#45b6a6;--brand-2:#6fa8e6;--brand-tint:#14322d;--brand-tint-border:#245349;--brand-tint-text:#7fd3c4;--good:#47c281;--good-tint:#12301f;--good-border:#245639;--warn:#e0aa4d;--warn-tint:#322810;--warn-border:#574413;--danger:#e6695c;--danger-tint:#35201d;--danger-border:#5c332d;--danger-text:#f0a49b;--purple:#ac90e2;--purple-tint:#221b31;--purple-border:#3d3357;--purple-text:#c9b6ef;--input-bg:#131a22;--btn-bg:#1e2731;--btn-text:#dbe2ea;--btn-hover:#26313d;--switch-track:#3a454f;--code-bg:#0c1218;--code-text:#cdd9e5;--code-border:#232f3b;--sidebar:#10151b;--sidebar-soft:#1c242f;--sidebar-line:#262f3a;--sidebar-row:#161d25;--sidebar-text:#cbd4de;--sidebar-muted:#7c8794;--sidebar-faint:#6f7b88;--sidebar-heading:#f0f4f8;--focus:#37a5963d;--shadow:0 16px 40px #00000075;--shadow-sm:0 2px 12px #00000061;--shadow-brand:0 8px 22px #37a59642}*{box-sizing:border-box}html,body,#root{min-height:100%}body{color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;transition:background-color .2s,color .2s}button,input,select,textarea{font:inherit}a{color:inherit;text-decoration:none}.shell{grid-template-columns:232px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{height:100vh;color:var(--sidebar-text);background:var(--sidebar);border-right:1px solid var(--sidebar-line);flex-direction:column;gap:16px;padding:18px 12px;display:flex;position:sticky;top:0;overflow-y:auto}.brand{align-items:center;gap:10px;min-height:42px;padding:0 4px;display:flex}.brand.compact{justify-content:center}.brand-elevated .brand-mark{box-shadow:var(--shadow-brand)}.brand-mark{color:#fff;background:var(--brand);border-radius:var(--radius-sm);border:1px solid #fff3;place-items:center;width:34px;height:34px;font-weight:800;display:grid}.brand strong{font-size:14px;line-height:1.1;display:block}.brand small{color:var(--sidebar-muted);margin-top:3px;font-size:11px;display:block}.sidebar-label{color:var(--sidebar-faint);text-transform:uppercase;letter-spacing:.04em;padding:0 8px;font-size:11px;font-weight:700}.nav-list,.nav-section{gap:4px;display:grid}.nav-section-toggle{width:100%;min-height:38px;color:var(--sidebar-muted);border-radius:var(--radius-sm);cursor:pointer;text-align:left;background:0 0;border:1px solid #0000;grid-template-columns:18px minmax(0,1fr) 16px;align-items:center;gap:9px;padding:0 10px;font-size:12px;font-weight:800;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-section-toggle:hover,.nav-section.active .nav-section-toggle{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-section-chevron{color:var(--sidebar-muted);justify-self:end;transition:transform .14s}.nav-section.open .nav-section-chevron{transform:rotate(180deg)}.nav-children{gap:4px;padding:2px 0 2px 18px;display:grid}.nav-item{min-height:38px;color:var(--sidebar-text);border-radius:var(--radius-sm);border:1px solid #0000;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:9px;padding:0 10px;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-dot{background:var(--sidebar-faint);border-radius:999px;justify-self:center;width:6px;height:6px}.nav-item:hover,.nav-item.active{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-item.active .nav-dot{background:var(--brand)}.sidebar-status{gap:7px;margin-top:auto;display:grid}.runtime-row{min-height:32px;color:var(--sidebar-text);background:var(--sidebar-row);border:1px solid var(--sidebar-line);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;padding:0 8px;display:grid}.runtime-row strong{color:var(--sidebar-heading);font-size:11px}.workspace{min-width:0}.topbar{z-index:20;background:var(--topbar-bg);border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);justify-content:space-between;align-items:center;gap:18px;min-height:66px;padding:12px 24px;display:flex;position:sticky;top:0}.topbar h1{color:var(--heading);margin:2px 0 0;font-size:20px;line-height:1.2}.topbar-actions,.page-actions,.section-action,.entity-badges,.row-actions,.modal-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.language-switch{background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;min-height:30px;padding:2px;display:inline-flex}.language-switch button{min-width:42px;min-height:24px;color:var(--muted);cursor:pointer;background:0 0;border:0;border-radius:999px;padding:0 9px;font-weight:800;transition:color .14s,background-color .14s}.language-switch button.active{color:#fff;background:var(--brand)}.language-switch button:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.theme-toggle{width:34px;height:34px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);cursor:pointer;border-radius:999px;place-items:center;transition:color .16s,background-color .16s,border-color .16s;display:inline-grid}.theme-toggle:hover{color:var(--brand);border-color:var(--brand-tint-border);background:var(--brand-tint)}.theme-toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.actor-pill{min-height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;padding:0 10px;display:inline-flex}.content{gap:16px;padding:18px 24px 30px;display:grid}.eyebrow{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:11px;font-weight:800}.dashboard-layout,.stacked-sections{gap:14px;display:grid}.overview-band,.page-frame{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm)}.overview-band{grid-template-columns:minmax(220px,1fr) minmax(420px,.9fr);align-items:center;gap:16px;padding:16px;display:grid}.overview-band h2,.page-title-row h2,.section-head h2,.modal h2{color:var(--heading);margin:0;font-size:18px;line-height:1.25}.overview-metrics,.metric-row{grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px;display:grid}.overview-metrics{grid-template-columns:repeat(3,minmax(120px,1fr))}.status-item,.metric,.summary-item{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);min-width:0;padding:10px}.status-item span,.metric span,.summary-item span{color:var(--muted);margin-bottom:6px;font-size:11px;display:block}.status-item strong,.metric strong,.summary-item strong{overflow-wrap:anywhere;color:var(--text);font-weight:800;display:block}.status-item.good,.metric.good{border-color:var(--good-border)}.status-item.warn,.metric.warn{border-color:var(--warn-border)}.metric.danger{border-color:var(--danger-border)}.command-grid{grid-template-columns:repeat(3,minmax(220px,1fr));gap:12px;display:grid}.launcher{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-height:94px;box-shadow:var(--shadow-sm);grid-template-columns:38px minmax(0,1fr) 18px;align-items:center;gap:12px;padding:14px;transition:border-color .16s,box-shadow .16s,transform .16s;display:grid}.launcher:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.launcher-icon{width:38px;height:38px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);place-items:center;display:grid}.launcher-copy{gap:4px;display:grid}.launcher-copy strong{color:var(--heading);font-size:15px}.launcher-copy span{color:var(--muted)}.work-strip{grid-template-columns:repeat(4,minmax(160px,1fr));gap:8px;display:grid}.strip-item{min-height:38px;color:var(--text-soft);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.page-frame{gap:14px;padding:14px;display:grid}.page-title-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:14px;padding-bottom:12px;display:flex}.query-panel{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);padding:10px}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.message-query input{width:150px}.message-selector-grid{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;margin-bottom:10px;display:grid}.message-selector-grid.single{grid-template-columns:minmax(320px,620px)}.entity-picker{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;min-width:0;padding:10px;display:grid}.picker-head{min-height:24px;color:var(--text-soft);justify-content:space-between;align-items:center;gap:8px;font-weight:800;display:flex}.selected-entity{min-height:40px;color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:8px;padding:7px 9px;display:grid}.selected-entity strong,.selected-entity span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.selected-entity div{gap:2px;min-width:0;display:grid}.selected-entity div span{color:var(--brand-tint-text);opacity:.85;font-size:11px}.picker-search{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;height:34px;padding:0 6px 0 9px;display:grid}.picker-search input{width:100%;height:30px;box-shadow:none;background:0 0;border:0;padding:0}.picker-results{border:1px solid var(--line);border-radius:var(--radius-sm);max-height:236px;display:grid;overflow:auto}.picker-row{min-height:36px;color:var(--text);background:var(--panel);border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;grid-template-columns:96px minmax(120px,1fr) minmax(120px,1fr) auto;align-items:center;gap:8px;padding:6px 8px;display:grid}.picker-row:last-child{border-bottom:0}.picker-row:hover,.picker-row.selected{background:var(--surface-soft)}.picker-row strong,.picker-row span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.picker-empty,.picker-error{color:var(--muted);text-align:center;padding:9px}.picker-error{color:var(--danger);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius-sm)}input,select,textarea{color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);outline:none;transition:border-color .14s,box-shadow .14s}input::placeholder,textarea::placeholder{color:var(--muted-2)}input,select{width:190px;height:34px;padding:0 10px}select{min-width:220px}textarea{resize:vertical;width:100%;padding:9px 10px}input:focus,select:focus,textarea:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus)}.small-input{width:88px}.field-inline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.field-inline span{font-size:11px;font-weight:700}.searchbox{width:min(380px,100%);height:34px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:inline-flex}.searchbox input{width:100%;height:30px;box-shadow:none;border:0;padding:0}.btn{min-height:34px;color:var(--btn-text);background:var(--btn-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:6px;padding:0 12px;transition:background-color .14s,border-color .14s,color .14s,box-shadow .14s;display:inline-flex}.btn:hover:not(:disabled){background:var(--btn-hover)}.btn:disabled{color:var(--muted-2);cursor:not-allowed}.btn.primary{color:#fff;background:var(--brand);border-color:var(--brand)}.btn.primary:hover:not(:disabled){background:var(--brand-strong);border-color:var(--brand-strong)}.btn.ghost{background:var(--panel-subtle)}.btn.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.btn.danger:hover:not(:disabled){background:var(--danger-tint);border-color:var(--danger)}.btn.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.btn.warn:hover:not(:disabled){background:var(--warn-tint);border-color:var(--warn)}.btn:disabled,.btn.primary:disabled,.btn.warn:disabled,.btn.danger:disabled{color:var(--muted-2);background:var(--panel-strong);border-color:var(--line);cursor:not-allowed}.btn.full{width:100%}.icon-text{gap:7px}.compact-btn{min-height:28px;padding:0 8px;font-size:12px}.row-link,.link-button{color:var(--brand-2);cursor:pointer;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.table-wrap{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);width:100%;overflow-x:auto}.data-table{border-collapse:collapse;width:100%;font-size:12.5px}.data-table th,.data-table td{border-bottom:1px solid var(--line);text-align:left;vertical-align:middle;white-space:nowrap;height:38px;padding:7px 9px}.data-table th{z-index:0;color:var(--muted);background:var(--panel-strong);font-weight:800;position:sticky;top:0}.data-table tbody tr:hover{background:var(--panel-subtle)}.data-table tr:last-child td{border-bottom:0}.mono{font-family:SFMono-Regular,Consolas,Liberation Mono,monospace}.truncate{text-overflow:ellipsis;max-width:380px;overflow:hidden}.badge{min-height:22px;color:var(--muted);background:var(--panel-strong);border:1px solid var(--line-strong);white-space:nowrap;border-radius:999px;align-items:center;padding:1px 8px;display:inline-flex}.badge.good{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.badge.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.badge.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.empty-cell{color:var(--muted);text-align:center}.bot-create-fields{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;display:grid}.bot-create-fields .duration-field input{width:100%}.bot-create-actions{border-top:1px solid var(--line);justify-content:space-between;align-items:center;gap:14px;margin-top:14px;padding-top:14px;display:flex}.bot-create-note{color:var(--muted);font-size:12px;line-height:1.4}@media (width<=760px){.bot-create-fields{grid-template-columns:1fr}.bot-create-actions{flex-direction:column;align-items:stretch}}.split-layout{grid-template-columns:minmax(0,1fr) 330px;align-items:start;gap:14px;display:grid}.split-main,.split-side{min-width:0}.entity-head{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);justify-content:space-between;align-items:flex-start;gap:14px;padding:14px;display:flex}.entity-title{color:var(--heading);font-size:20px;font-weight:800;line-height:1.25}.entity-subtitle{color:var(--muted);margin-top:4px}.summary-grid{grid-template-columns:repeat(4,minmax(150px,1fr));gap:8px;display:grid}.about-text{color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);margin:0;padding:10px}.section-block,.action-dock,.surface{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm);padding:12px}.section-head{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;display:flex}.section-head p{color:var(--muted);margin:5px 0 0}.action-dock{gap:10px;display:grid;position:sticky;top:82px}.dock-title{color:var(--text-soft);border-bottom:1px solid var(--line);padding-bottom:4px;font-weight:800}.action-dock>.btn,.action-dock .action-stack .btn{justify-content:center;width:100%}.duration-field{gap:4px;display:grid}.duration-field span{color:var(--muted);font-size:11px;font-weight:800}.duration-field input{width:100%}.action-stack{gap:10px;display:grid}.action-stack .btn,.action-dock>.btn{min-height:42px}.danger-zone{border-top:1px solid var(--line);flex-wrap:wrap;gap:8px;margin-top:10px;padding-top:10px;display:flex}.authorization-block{gap:10px;display:grid}.authorization-table{table-layout:fixed;min-width:720px}.authorization-table th,.authorization-table td{height:46px}.device-text{text-overflow:ellipsis;max-width:260px;overflow:hidden}.device-actions-head{width:190px}.device-actions-cell{width:190px;min-width:190px}.device-actions{white-space:normal;grid-template-columns:repeat(2,minmax(82px,1fr));gap:6px;min-width:178px;display:grid}.device-actions .btn{justify-content:center;width:100%}.operation-row{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;display:grid}.operation-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);flex-wrap:wrap;align-items:center;gap:8px;padding:10px;display:flex}.operation-title{width:100%;color:var(--heading);align-items:center;gap:6px;font-weight:800;display:flex}.checkline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.checkline input{width:auto;height:auto}.alert{color:var(--danger-text);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius);align-items:flex-start;gap:8px;padding:9px 10px;display:flex}.json-block{max-height:520px;color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius);margin:0;padding:12px;font-size:12px;overflow:auto}.raw-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.loading-line{min-height:80px;color:var(--muted);place-items:center;display:grid}.empty-panel{min-height:92px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);place-items:center;display:grid}.gift-metrics .metric{background:var(--panel-subtle);min-height:68px;padding:12px}.gift-metrics .metric strong{font-size:17px}.gift-file-icon{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);flex:none;place-items:center;display:grid}.gift-format-chips{flex-wrap:wrap;flex:none;justify-content:flex-end;gap:6px;display:flex}.gift-format-chips span{color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);letter-spacing:.02em;border-radius:999px;padding:4px 8px;font-size:10px;font-weight:800}.gift-list-summary{color:var(--muted);margin-left:auto;font-size:11px;font-weight:700}.gift-import-modal{width:min(860px,100%)}.gift-import-modal-body{gap:14px}.gift-source-tabs{gap:8px;display:flex}.give-gift-summary{background:var(--panel-subtle);border:1px solid var(--line-strong);color:var(--text-soft);border-radius:12px;align-items:center;gap:11px;padding:11px 13px;display:flex}.give-gift-summary>svg{color:var(--brand);flex:none}.give-gift-summary strong{color:var(--text);font-size:13px;display:block}.give-gift-summary .mono{color:var(--muted);font-size:11px}.give-gift-tabs{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:12px;gap:4px;width:100%;padding:4px;display:flex}.give-gift-tabs .btn{min-height:36px;box-shadow:none;color:var(--text-soft);background:0 0;border:1px solid #0000;border-radius:9px;flex:1 1 0;justify-content:center;transition:color .15s,background .15s,border-color .15s,box-shadow .15s}.give-gift-tabs .btn:not(.primary):hover{color:var(--brand);background:var(--brand-tint)}.give-gift-tabs .btn.primary{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.give-gift-upgrade-note{background:var(--brand-tint);border:1px solid var(--brand-tint-border);color:var(--text-soft);border-radius:10px;margin:0;padding:9px 12px;font-size:11px;font-weight:650;line-height:1.45}.give-gift-attrs{grid-template-columns:repeat(3,minmax(0,1fr));align-items:end}.give-gift-attrs select,.give-gift-attrs input{width:100%;min-width:0;height:38px;color:var(--text);background-color:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;appearance:none;cursor:pointer;padding:0 32px 0 10px;font-size:12px;font-weight:600}.give-gift-attrs input{cursor:text;text-overflow:ellipsis;padding-right:10px}.give-gift-attrs select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239aa4b2' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-position:right 11px center;background-repeat:no-repeat}.give-gift-attrs select:focus,.give-gift-attrs input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.give-gift-layout{grid-template-columns:minmax(220px,280px) minmax(0,1fr);align-items:start;gap:16px;display:grid}.give-gift-picker{align-content:start;gap:10px;display:grid}.give-gift-picker-head{align-items:center;gap:12px;display:flex}.give-gift-picker-head .searchbox{flex:auto}.give-gift-picker-list{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-lg);gap:8px;max-height:640px;padding:8px;display:grid;overflow-y:auto}.give-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);grid-template-columns:46px minmax(0,1fr) auto;align-items:center;gap:11px;padding:9px 11px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.give-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.give-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.give-gift-thumb{place-items:center;width:46px;height:46px;display:grid}.give-gift-thumb canvas{width:100%!important;height:100%!important}.give-gift-option-info{gap:3px;min-width:0;display:grid}.give-gift-option-info strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.give-gift-option-info .mono{color:var(--muted);font-size:10px}.give-gift-option-price{white-space:nowrap;justify-self:end}.give-gift-panel{background:var(--panel);border:1px solid var(--line-strong);border-radius:var(--radius-lg);gap:12px;min-width:0;padding:16px;display:grid}.give-gift-form{gap:12px;min-width:0;display:grid}.give-gift-form-actions{flex-wrap:wrap;justify-content:flex-end;gap:10px;padding-top:4px;display:flex}.give-gift-empty-panel{color:var(--muted);text-align:center;place-items:center;gap:10px;padding:48px 20px;display:grid}.give-gift-empty-panel svg{color:var(--brand);opacity:.8}.official-gift-picker{gap:12px;min-width:0;display:grid}.official-gift-tools{align-items:center;gap:12px;display:flex}.official-gift-tools .searchbox{width:100%}.official-gift-tools>span{color:var(--muted);flex:none;font-size:11px;font-weight:750}.official-gift-categories{flex-wrap:wrap;gap:7px;display:flex}.official-gift-categories button{min-height:32px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line-strong);font:inherit;cursor:pointer;border-radius:999px;align-items:center;gap:7px;padding:5px 10px;font-size:11px;font-weight:800;transition:color .15s,background .15s,border-color .15s,box-shadow .15s;display:inline-flex}.official-gift-categories button:hover{color:var(--brand);border-color:var(--brand-tint-border)}.official-gift-categories button.active{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.official-gift-categories button span{min-width:20px;height:20px;color:inherit;background:#7d8c9b38;border-radius:999px;place-items:center;padding:0 5px;font-size:10px;display:grid}.official-gift-categories button.active span{color:var(--brand);background:#ffffffd9}.official-gift-list{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--panel-subtle);scrollbar-gutter:stable;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;min-height:126px;max-height:314px;padding:8px;display:grid;overflow:auto}.official-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);gap:8px;padding:11px 12px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.official-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.official-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.official-gift-option-head{grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:8px;display:grid}.official-gift-option-head strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.official-gift-option-head .mono{color:var(--muted);font-size:9px}.official-gift-option-meta{color:var(--muted);flex-wrap:wrap;gap:10px;font-size:10px;font-weight:700;display:flex}.official-gift-capabilities{flex-wrap:wrap;gap:5px;display:flex}.official-gift-capabilities>span{letter-spacing:.01em;border:1px solid #0000;border-radius:999px;padding:3px 7px;font-size:9px;font-weight:850}.official-gift-capabilities>span.yes{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.official-gift-capabilities>span.craft{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.official-gift-capabilities>span.no{color:var(--muted);background:var(--panel-strong);border-color:var(--line-strong)}.official-gift-empty{min-height:108px;color:var(--muted);text-align:center;grid-column:1/-1;place-items:center;padding:20px;font-size:12px;display:grid}.official-gift-selected{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--surface-soft);grid-template-columns:108px minmax(0,1fr);align-items:center;gap:14px;padding:12px;display:grid}.official-gift-selected .gift-animation-shell{width:96px;height:96px}.official-gift-selected>div:last-child{gap:5px;min-width:0;display:grid}.official-gift-selected small{color:var(--muted)}.gift-import-note{color:var(--muted);justify-content:space-between;align-items:center;gap:12px;line-height:1.45;display:flex}.gift-file-picker{min-height:78px;color:var(--text);background:var(--panel);border:1px dashed var(--line-strong);border-radius:var(--radius);cursor:pointer;grid-template-columns:42px minmax(0,1fr) auto;align-items:center;gap:12px;padding:12px 14px;transition:border-color .16s,background .16s,box-shadow .16s;display:grid;position:relative}.gift-file-picker:hover,.gift-file-picker.has-file{background:var(--brand-tint);border-color:var(--brand);box-shadow:0 0 0 2px var(--focus)}.gift-file-picker input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.gift-file-icon{border-radius:var(--radius-sm);width:40px;height:40px}.gift-file-copy{gap:2px;min-width:0;display:grid}.gift-field-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:10px;font-weight:800}.gift-file-copy strong{color:var(--heading);text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.gift-file-copy small{color:var(--muted);font-size:11px;font-weight:500}.gift-file-action{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);padding:7px 10px;font-size:11px;font-weight:800}.gift-fields-grid{grid-template-columns:minmax(200px,1.5fr) repeat(3,minmax(120px,1fr));gap:10px;display:grid}.gift-fields-grid label,.gift-reason-field{color:var(--muted);gap:6px;font-size:11px;font-weight:700;display:grid}.gift-fields-grid input,.gift-reason-field input{width:100%;min-width:0;height:38px;color:var(--text);background:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);padding:0 10px}.gift-fields-grid input:focus,.gift-reason-field input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.gift-switch{color:var(--text-soft);cursor:pointer;align-items:center;gap:9px;font-size:12px;font-weight:700;display:inline-flex}.gift-switch input{opacity:0;width:1px;height:1px;position:absolute}.gift-switch-track{background:var(--switch-track);border-radius:999px;align-items:center;width:34px;height:19px;padding:2px;transition:background .16s;display:flex}.gift-switch-track span{background:#fff;border-radius:50%;width:15px;height:15px;transition:transform .16s;box-shadow:0 1px 3px #10182838}.gift-switch input:checked+.gift-switch-track{background:var(--brand)}.gift-switch input:checked+.gift-switch-track span{transform:translate(15px)}.gift-switch input:focus-visible+.gift-switch-track{outline:3px solid var(--focus);outline-offset:2px}.gift-validation{color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius-sm);overflow:hidden}.gift-validation-head{color:var(--code-text);background:#ffffff09;border-bottom:1px solid #ffffff17;align-items:center;gap:9px;padding:10px 12px;display:flex}.gift-validation-head div{gap:2px;display:grid}.gift-validation-head span{color:var(--brand);font-size:10px}.gift-validation pre{max-height:180px;color:var(--code-text);margin:0;padding:11px 12px;font-size:11px;overflow:auto}.gift-animation-shell{background:var(--surface-soft);place-items:center;min-height:210px;display:grid;position:relative}.gift-animation{width:200px;height:200px}.gift-animation canvas{width:100%!important;height:100%!important}.gift-play{width:30px;height:30px;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:50%;place-items:center;display:grid;position:absolute;bottom:8px;right:8px}.gift-table-wrap{background:var(--panel)}.gift-table{min-width:1080px}.gift-table th:first-child{width:74px}.gift-table td{vertical-align:middle}.gift-animation-shell.compact{border:1px solid var(--line);border-radius:var(--radius-sm);width:56px;min-height:56px;overflow:hidden}.gift-animation-shell.compact .gift-animation{width:54px;height:54px}.gift-animation-shell.compact .gift-play{width:20px;height:20px;bottom:3px;right:3px}.gift-row-disabled{opacity:.68}.gift-table-title,.gift-sort-order,.gift-source-size,.gift-convert-price{display:block}.gift-table-title{text-overflow:ellipsis;white-space:nowrap;max-width:220px;overflow:hidden}.gift-sort-order,.gift-source-size,.gift-convert-price{color:var(--muted);margin-top:3px;font-size:10px}.gift-table-price{color:var(--warn)}.gift-table-actions{align-items:center;gap:6px;display:flex}.collectible-button{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.collectible-button:hover{background:var(--purple-tint);border-color:var(--purple)}.collectible-modal{width:min(1180px,100%);max-height:min(92vh,980px)}.collectible-modal .modal-head p{color:var(--muted);margin:4px 0 0;font-size:11px}.collectible-modal-body{background:var(--bg);gap:16px;padding:16px 18px 22px;overflow:auto}.collectible-loading{min-height:90px;color:var(--muted);justify-content:center;align-items:center;gap:8px;display:flex}.collectible-empty{color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius);align-items:center;gap:12px;padding:16px;display:flex}.collectible-empty div,.collectible-definition-head>div:first-child,.collectible-section-head>div:first-child{gap:3px;display:grid}.collectible-empty span,.collectible-definition-head span,.collectible-section-head span{color:var(--muted);font-size:10px;font-weight:500}.collectible-active{background:var(--panel);border:1px solid var(--purple-border);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-active-head{background:var(--purple-tint);border-bottom:1px solid var(--purple-border);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.collectible-active-head>div{color:var(--purple-text);align-items:center;gap:9px;display:flex}.collectible-active-head>div>div{gap:2px;display:grid}.collectible-active-head span{color:var(--muted);font-size:10px}.collectible-active-grid{background:var(--line);grid-template-columns:repeat(auto-fill,minmax(145px,1fr));gap:1px;display:grid}.collectible-active-grid article{background:var(--panel);align-items:center;gap:9px;min-width:0;padding:9px 11px;display:flex}.collectible-active-grid article>div:last-child{gap:2px;min-width:0;display:grid}.collectible-active-grid article strong{text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.collectible-active-grid article span{color:var(--muted);font-size:9px}.collectible-definition{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-definition-head{background:var(--panel-subtle);border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.collectible-main-fields{background:var(--panel-subtle);border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section{border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section:last-child{border-bottom:0}.collectible-section-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;display:flex}.collectible-section-tools{align-items:center;gap:7px;display:flex}.collectible-rows{gap:7px;display:grid}.collectible-row{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:end;gap:7px;padding:9px 9px 9px 36px;display:grid;position:relative}.collectible-row:hover{background:var(--panel);border-color:var(--line-strong);box-shadow:var(--shadow-sm)}.collectible-row.animated{grid-template-columns:minmax(120px,1.2fr) 90px 78px minmax(160px,1.4fr) 48px 30px}.collectible-row.backdrop{grid-template-columns:minmax(110px,1.2fr) 70px 80px 70px repeat(4,52px) 48px 30px}.collectible-row-index{width:27px;color:var(--purple-text);background:var(--purple-tint);border-right:1px solid var(--purple-border);border-radius:var(--radius-xs) 0 0 var(--radius-xs);place-items:center;font-size:10px;font-weight:800;display:grid;position:absolute;top:0;bottom:0;left:0}.collectible-row label{gap:4px;min-width:0;display:grid}.collectible-row label>span{color:var(--muted);text-transform:uppercase;letter-spacing:.025em;font-size:9px;font-weight:800}.collectible-row input:not([type=file]){width:100%;min-width:0;height:32px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font:inherit;padding:0 8px;font-size:11px}.collectible-row input:focus{border-color:var(--purple);box-shadow:0 0 0 3px var(--purple-tint);outline:none}.collectible-file input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.collectible-file em{min-width:0;height:32px;color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;cursor:pointer;align-items:center;gap:5px;padding:0 8px;font-size:10px;font-style:normal;font-weight:700;display:flex;overflow:hidden}.collectible-inline-preview{width:42px;height:42px;color:var(--purple);background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);place-items:center;display:grid;overflow:hidden}.collectible-animation{width:100%;height:100%;overflow:hidden}.collectible-animation.compact{background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);flex:0 0 42px;place-items:center;width:42px;height:42px;display:grid}.collectible-animation canvas{width:100%!important;height:100%!important}.collectible-animation.failed{color:var(--danger);background:var(--danger-tint)}.collectible-animation.loading{color:var(--purple-text)}.collectible-file-error{color:var(--danger);grid-column:1/-1;font-size:10px}.collectible-color input{cursor:pointer;height:32px!important;padding:3px!important}.collectible-backdrop-preview{border-radius:var(--radius-sm);border:1px solid #2a1f472e;flex:0 0 42px;place-items:center;width:42px;height:42px;font-size:11px;font-weight:900;display:grid;box-shadow:inset 0 0 0 1px #fff3}.collectible-row .icon-btn{align-self:center}.collectible-row .icon-btn:disabled{opacity:.28}@media (width<=900px){.gift-fields-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.give-gift-layout{grid-template-columns:1fr}.give-gift-picker-list{max-height:320px}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:repeat(2,minmax(0,1fr))}.collectible-inline-preview,.collectible-backdrop-preview,.collectible-row .icon-btn{place-self:center start}}@media (width<=620px){.gift-import-note{flex-direction:column;align-items:flex-start}.gift-format-chips{justify-content:flex-start}.gift-file-picker{grid-template-columns:40px minmax(0,1fr)}.gift-file-action{display:none}.gift-fields-grid{grid-template-columns:1fr}.gift-list-summary{width:100%;margin-left:0}.official-gift-tools{flex-direction:column;align-items:stretch}.official-gift-list{grid-template-columns:1fr;max-height:340px}.official-gift-selected{grid-template-columns:82px minmax(0,1fr)}.official-gift-selected .gift-animation-shell{width:72px;height:72px}.collectible-modal-body{padding:10px}.collectible-definition-head,.collectible-section-head{flex-direction:column;align-items:flex-start}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:1fr}.collectible-active-grid{grid-template-columns:1fr 1fr}}.attr-block{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;padding:10px;display:grid}.attr-block .duration-field input{width:100%}.attr-block .btn{justify-content:center;width:100%}.emoji-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;display:grid}.emoji-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);gap:8px;padding:12px;display:grid}.emoji-preview{background:var(--surface-soft);border:1px solid var(--line);border-radius:var(--radius-sm);place-items:center;height:88px;display:grid}.emoji-anim{width:80px;height:80px}.emoji-anim canvas{width:100%!important;height:100%!important}.emoji-glyph{font-size:46px;line-height:1}.emoji-meta{gap:4px;min-width:0;display:grid}.emoji-alt{font-size:18px;line-height:1.2}.emoji-id{width:100%;min-width:0;color:var(--text);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;justify-content:space-between;align-items:center;gap:6px;padding:4px 8px;font-size:11px;display:flex}.emoji-id .mono{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.emoji-id svg{flex:none}.emoji-id:hover{border-color:var(--brand-tint-border);color:var(--brand)}.emoji-sub{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.modal-backdrop{z-index:10000;background:var(--overlay);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(760px,100%);max-height:min(820px,100vh - 48px);box-shadow:var(--shadow);padding:0;overflow:hidden}.command-modal{flex-direction:column;display:flex}.command-modal>.modal-head,.command-modal>.modal-actions{flex:none}.modal-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 18px 12px;display:flex}.icon-btn{width:30px;height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;place-items:center;transition:background-color .14s,border-color .14s,color .14s;display:grid}.icon-btn:hover{background:var(--btn-hover);border-color:var(--line-strong)}.command-steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.command-body{grid-auto-rows:max-content;gap:12px;min-height:0;padding:14px 18px;display:grid;overflow:auto}.command-step{min-height:38px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.command-step span{background:var(--panel);border:1px solid var(--line);border-radius:999px;place-items:center;width:20px;height:20px;font-size:11px;font-weight:800;display:grid}.command-step.active{color:var(--brand);border-color:var(--brand-tint-border)}.command-step.done{color:var(--good);border-color:var(--good-border)}.form-field{gap:6px;display:grid}.form-field span,.form-stack span{color:var(--text-soft);font-weight:800}.form-field input:disabled,.form-field textarea:disabled{opacity:.6;cursor:not-allowed}.command-preview{gap:8px;display:grid}.command-preview .json-block{max-height:150px}.preview-head,.result-title{color:var(--text-soft);align-items:center;gap:7px;font-weight:800;display:flex}.result-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);gap:8px;padding:10px;display:grid}.result-line{grid-template-columns:92px minmax(0,1fr);gap:8px;display:grid}.result-line span{color:var(--muted)}.result-line strong{overflow-wrap:anywhere}.result-message{color:var(--text-soft)}.modal-actions{background:var(--panel);border-top:1px solid var(--line);justify-content:flex-end;padding:12px 18px}.login-page{background:var(--bg);place-items:center;min-height:100vh;padding:24px;display:grid}.login-panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(420px,100%);box-shadow:var(--shadow);gap:18px;padding:22px;display:grid}.login-head{justify-content:space-between;align-items:center;gap:12px;display:flex}.login-head-actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.login-chip{min-height:24px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:999px;align-items:center;padding:0 8px;font-size:12px;display:inline-flex}.login-copy h1{color:var(--heading);margin:0;font-size:22px}.login-copy p{color:var(--muted);margin:8px 0 0}.form-stack{gap:12px;display:grid}.form-stack label{gap:6px;display:grid}.form-stack input{width:100%}.boot-screen{background:var(--bg);align-content:center;place-items:center;gap:18px;min-height:100vh;display:grid}.loader-bar{background:var(--line-strong);border-radius:999px;width:180px;height:4px;overflow:hidden}.loader-bar:before{content:"";background:var(--brand);width:42%;height:100%;animation:1s ease-in-out infinite load;display:block}.spin{animation:.8s linear infinite spin}@keyframes load{0%{transform:translate(-120%)}to{transform:translate(260%)}}@keyframes spin{to{transform:rotate(360deg)}}@media (width<=1120px){.shell{grid-template-columns:1fr}.sidebar{height:auto;position:static}.nav-list{grid-template-columns:repeat(4,minmax(0,1fr))}.sidebar-status{display:none}.overview-band,.split-layout,.operation-row,.raw-grid,.message-selector-grid,.message-selector-grid.single{grid-template-columns:1fr}.action-dock{position:static}}@media (width<=760px){.content,.topbar{padding-left:14px;padding-right:14px}.command-grid,.work-strip,.overview-metrics,.metric-row,.summary-grid,.command-steps{grid-template-columns:1fr}.sidebar{gap:12px;padding:14px}.nav-list{grid-template-columns:repeat(2,minmax(0,1fr))}.topbar,.page-title-row,.entity-head{flex-direction:column;align-items:flex-start}input,.searchbox{width:100%}.toolbar{align-items:stretch}.picker-row,.selected-entity{grid-template-columns:1fr}} diff --git a/cmd/telesrv-admin/web/dist/assets/index-DJw3UpEg.js b/cmd/telesrv-admin/web/dist/assets/index-DJw3UpEg.js deleted file mode 100644 index 65727560..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-DJw3UpEg.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{re=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:``)?ne(e):``}function ae(e){switch(e.tag){case 5:return ne(e.type);case 16:return ne(`Lazy`);case 13:return ne(`Suspense`);case 19:return ne(`SuspenseList`);case 0:case 2:case 15:return e=ie(e.type,!1),e;case 11:return e=ie(e.type.render,!1),e;case 1:return e=ie(e.type,!0),e;default:return``}}function oe(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?oe(e.type)||`Memo`:t;case F:t=e._payload,e=e._init;try{return oe(e(t))}catch{}}return null}function se(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 oe(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 ce(e){switch(typeof e){case`boolean`:case`number`:case`string`:case`undefined`:return e;case`object`:return e;default:return``}}function le(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()===`input`&&(t===`checkbox`||t===`radio`)}function ue(e){var t=le(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 z(e){e._valueTracker||=ue(e)}function de(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r=``;return e&&(r=le(e)?e.checked?`true`:`false`:e.value),e=r,e===n?!1:(t.setValue(e),!0)}function B(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 fe(e,t){var n=t.checked;return R({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function pe(e,t){var n=t.defaultValue==null?``:t.defaultValue,r=t.checked==null?t.defaultChecked:t.checked;n=ce(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 me(e,t){t=t.checked,t!=null&&S(e,`checked`,t,!1)}function he(e,t){me(e,t);var n=ce(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`)?_e(e,t.type,n):t.hasOwnProperty(`defaultValue`)&&_e(e,t.type,ce(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function ge(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 _e(e,t,n){(t!==`number`||B(e.ownerDocument)!==e)&&(n==null?e.defaultValue=``+e._wrapperState.initialValue:e.defaultValue!==``+n&&(e.defaultValue=``+n))}var ve=Array.isArray;function ye(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i`+t.valueOf().toString()+``,t=Te.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function De(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Oe={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},ke=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(Oe).forEach(function(e){ke.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Oe[t]=Oe[e]})});function Ae(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||Oe.hasOwnProperty(e)&&Oe[e]?(``+t).trim():t+`px`}function je(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=Ae(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var Me=R({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 H(e,t){if(t){if(Me[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 Ne(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 Pe=null;function Fe(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Ie=null,U=null,Le=null;function Re(e){if(e=ji(e)){if(typeof Ie!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Ni(t),Ie(e.stateNode,e.type,t))}}function ze(e){U?Le?Le.push(e):Le=[e]:U=e}function Be(){if(U){var e=U,t=Le;if(Le=U=null,Re(e),t)for(e=0;e>>=0,e===0?32:31-(_t(e)/vt|0)|0}var bt=64,xt=4194304;function St(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 Ct(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=St(a))):r=St(s)}else o=n&~i,o===0?a!==0&&(r=St(a)):r=St(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 kt(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-gt(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=Gn),Jn=` `,Yn=!1;function Xn(e,t){switch(e){case`keyup`:return Un.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Zn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Qn=!1;function $n(e,t){switch(e){case`compositionend`:return Zn(t);case`keypress`:return t.which===32?(Yn=!0,Jn):null;case`textInput`:return e=t.data,e===Jn&&Yn?null:e;default:return null}}function er(e,t){if(Qn)return e===`compositionend`||!Wn&&Xn(e,t)?(e=mn(),pn=fn=dn=null,Qn=!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=xr(n)}}function Cr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Cr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function wr(){for(var e=window,t=B();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=B(e.document)}return t}function Tr(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 Er(e){var t=wr(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Cr(n.ownerDocument.documentElement,n)){if(r!==null&&Tr(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=Sr(n,a);var o=Sr(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,Or=null,kr=null,Ar=null,jr=!1;function Mr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;jr||Or==null||Or!==B(r)||(r=Or,`selectionStart`in r&&Tr(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}),Ar&&br(Ar,r)||(Ar=r,r=ii(kr,`onSelect`),0Fi||(e.current=Pi[Fi],Pi[Fi]=null,Fi--)}function Ri(e,t){Fi++,Pi[Fi]=e.current,e.current=t}var zi={},Bi=Ii(zi),Vi=Ii(!1),Hi=zi;function Ui(e,t){var n=e.type.contextTypes;if(!n)return zi;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 Wi(e){return e=e.childContextTypes,e!=null}function Gi(){Li(Vi),Li(Bi)}function Ki(e,t,n){if(Bi.current!==zi)throw Error(r(168));Ri(Bi,t),Ri(Vi,n)}function qi(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,se(e)||`Unknown`,a));return R({},n,i)}function Ji(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||zi,Hi=Bi.current,Ri(Bi,e),Ri(Vi,Vi.current),!0}function Yi(e,t,n){var i=e.stateNode;if(!i)throw Error(r(169));n?(e=qi(e,t,Hi),i.__reactInternalMemoizedMergedChildContext=e,Li(Vi),Li(Bi),Ri(Bi,e)):Li(Vi),Ri(Vi,n)}var Xi=null,Zi=!1,Qi=!1;function $i(e){Xi===null?Xi=[e]:Xi.push(e)}function ea(e){Zi=!0,$i(e)}function ta(){if(!Qi&&Xi!==null){Qi=!0;var e=0,t=X;try{var n=Xi;for(X=1;e>=o,i-=o,la=1<<32-gt(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),_a&&da(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),_a&&da(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 _a&&da(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)}),_a&&da(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&&ja(c)===l.type){n(e,l.sibling),r=a(l,i.props),r.ref=ka(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=ka(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(ve(i))return h(e,r,i,o);if(ee(i))return g(e,r,i,o);Aa(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 Na=Ma(!0),Pa=Ma(!1),Fa=Ii(null),Ia=null,La=null,Ra=null;function za(){Ra=La=Ia=null}function Ba(e){var t=Fa.current;Li(Fa),e._currentValue=t}function Va(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 Ha(e,t){Ia=e,Ra=La=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Ms=!0),e.firstContext=null)}function Ua(e){var t=e._currentValue;if(Ra!==e)if(e={context:e,memoizedValue:t,next:null},La===null){if(Ia===null)throw Error(r(308));La=e,Ia.dependencies={lanes:0,firstContext:e}}else La=La.next=e;return t}var Wa=null;function Ga(e){Wa===null?Wa=[e]:Wa.push(e)}function Ka(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,Ga(t)):(n.next=i.next,i.next=n),t.interleaved=n,qa(e,r)}function qa(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 Ja=!1;function Ya(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Xa(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 Za(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Qa(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,$&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,qa(e,n)}return i=r.interleaved,i===null?(t.next=t,Ga(r)):(t.next=i.next,i.next=t),r.interleaved=t,qa(e,n)}function $a(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 eo(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 to(e,t,n,r){var i=e.updateQueue;Ja=!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=R({},d,f);break a;case 2:Ja=!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 no(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=vo.transition;vo.transition={};try{e(!1),t()}finally{X=n,vo.transition=r}}function as(){return Mo().memoizedState}function os(e,t,n){var r=pl(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},cs(e))ls(t,n);else if(n=Ka(e,t,n,r),n!==null){var i=fl();ml(n,e,r,i),us(n,t,r)}}function ss(e,t,n){var r=pl(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(cs(e))ls(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,Z(s,o)){var c=t.interleaved;c===null?(i.next=i,Ga(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}n=Ka(e,t,i,r),n!==null&&(i=fl(),ml(n,e,r,i),us(n,t,r))}}function cs(e){var t=e.alternate;return e===bo||t!==null&&t===bo}function ls(e,t){wo=Co=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function us(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,jt(e,n)}}var ds={readContext:Ua,useCallback:Do,useContext:Do,useEffect:Do,useImperativeHandle:Do,useInsertionEffect:Do,useLayoutEffect:Do,useMemo:Do,useReducer:Do,useRef:Do,useState:Do,useDebugValue:Do,useDeferredValue:Do,useTransition:Do,useMutableSource:Do,useSyncExternalStore:Do,useId:Do,unstable_isNewReconciler:!1},fs={readContext:Ua,useCallback:function(e,t){return jo().memoizedState=[e,t===void 0?null:t],e},useContext:Ua,useEffect:Jo,useImperativeHandle:function(e,t,n){return n=n==null?null:n.concat([e]),Ko(4194308,4,Qo.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Ko(4194308,4,e,t)},useInsertionEffect:function(e,t){return Ko(4,2,e,t)},useMemo:function(e,t){var n=jo();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=jo();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=os.bind(null,bo,e),[r.memoizedState,e]},useRef:function(e){var t=jo();return e={current:e},t.memoizedState=e},useState:Uo,useDebugValue:es,useDeferredValue:function(e){return jo().memoizedState=e},useTransition:function(){var e=Uo(!1),t=e[0];return e=is.bind(null,e[1]),jo().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var i=bo,a=jo();if(_a){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Vc===null)throw Error(r(349));yo&30||Ro(i,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,Jo(Bo.bind(null,i,o,e),[e]),i.flags|=2048,Wo(9,zo.bind(null,i,o,n,t),void 0,null),n},useId:function(){var e=jo(),t=Vc.identifierPrefix;if(_a){var n=ua,r=la;n=(r&~(1<<32-gt(r)-1)).toString(32)+n,t=`:`+t+`R`+n,n=To++,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[wi]=t,e[Ti]=i,nc(e,t,!1,!1),t.stateNode=e;a:{switch(c=Ne(n,i),n){case`dialog`:Zr(`cancel`,e),Zr(`close`,e),o=i;break;case`iframe`:case`object`:case`embed`:Zr(`load`,e),o=i;break;case`video`:case`audio`:for(o=0;oel&&(t.flags|=128,i=!0,ac(s,!1),t.lanes=4194304)}else{if(!i)if(e=mo(c),e!==null){if(t.flags|=128,i=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),ac(s,!0),s.tail===null&&s.tailMode===`hidden`&&!c.alternate&&!_a)return oc(t),null}else 2*K()-s.renderingStartTime>el&&n!==1073741824&&(t.flags|=128,i=!0,ac(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?(oc(t),null):(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=K(),t.sibling=null,n=po.current,Ri(po,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&&(oc(t),t.subtreeFlags&6&&(t.flags|=8192)):oc(t),null;case 24:return null;case 25:return null}throw Error(r(156,t.tag))}function cc(e,t){switch(ma(t),t.tag){case 1:return Wi(t.type)&&Gi(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return lo(),Li(Vi),Li(Bi),go(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return fo(t),null;case 13:if(Li(po),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));Ea()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Li(po),null;case 4:return lo(),null;case 10:return Ba(t.type._context),null;case 22:case 23:return wl(),null;case 24:return null;default:return null}}var lc=!1,uc=!1,dc=typeof WeakSet==`function`?WeakSet:Set,Q=null;function fc(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 pc(e,t,n){try{n()}catch(n){Rl(e,t,n)}}var mc=!1;function hc(e,t){if(fi=rn,e=wr(),Tr(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(pi={focusedElem:e,selectionRange:n},rn=!1,Q=t;Q!==null;)if(t=Q,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,Q=e;else for(;Q!==null;){t=Q;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:hs(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,Q=e;break}Q=t.return}return h=mc,mc=!1,h}function gc(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&&pc(t,n,a)}i=i.next}while(i!==r)}}function _c(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 vc(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 yc(e){var t=e.alternate;t!==null&&(e.alternate=null,yc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[wi],delete t[Ti],delete t[Di],delete t[Oi],delete t[ki])),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 bc(e){return e.tag===5||e.tag===3||e.tag===4}function xc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||bc(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 Sc(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=di));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}function Cc(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(Cc(e,t,n),e=e.sibling;e!==null;)Cc(e,t,n),e=e.sibling}var wc=null,Tc=!1;function Ec(e,t,n){for(n=n.child;n!==null;)Dc(e,t,n),n=n.sibling}function Dc(e,t,n){if(mt&&typeof mt.onCommitFiberUnmount==`function`)try{mt.onCommitFiberUnmount(pt,n)}catch{}switch(n.tag){case 5:uc||fc(n,t);case 6:var r=wc,i=Tc;wc=null,Ec(e,t,n),wc=r,Tc=i,wc!==null&&(Tc?(e=wc,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):wc.removeChild(n.stateNode));break;case 18:wc!==null&&(Tc?(e=wc,n=n.stateNode,e.nodeType===8?bi(e.parentNode,n):e.nodeType===1&&bi(e,n),tn(e)):bi(wc,n.stateNode));break;case 4:r=wc,i=Tc,wc=n.stateNode.containerInfo,Tc=!0,Ec(e,t,n),wc=r,Tc=i;break;case 0:case 11:case 14:case 15:if(!uc&&(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)&&pc(n,t,o),i=i.next}while(i!==r)}Ec(e,t,n);break;case 1:if(!uc&&(fc(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)}Ec(e,t,n);break;case 21:Ec(e,t,n);break;case 22:n.mode&1?(uc=(r=uc)||n.memoizedState!==null,Ec(e,t,n),uc=r):Ec(e,t,n);break;default:Ec(e,t,n)}}function Oc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new dc),t.forEach(function(t){var r=Hl.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))})}}function kc(e,t){var n=t.deletions;if(n!==null)for(var i=0;ia&&(a=s),i&=~o}if(i=a,i=K()-i,i=(120>i?120:480>i?480:1080>i?1080:1920>i?1920:3e3>i?3e3:4320>i?4320:1960*Lc(i/1960))-i,10e?16:e,ol===null)var i=!1;else{if(e=ol,ol=null,sl=0,$&6)throw Error(r(331));var a=$;for($|=4,Q=e.current;Q!==null;){var o=Q,s=o.child;if(Q.flags&16){var c=o.deletions;if(c!==null){for(var l=0;lK()-$c?Tl(e,0):Xc|=n),hl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=xt,xt<<=1,!(xt&130023424)&&(xt=4194304)):t=1);var n=fl();e=qa(e,t),e!==null&&(kt(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||Vi.current)Ms=!0;else{if((e.lanes&n)===0&&!(t.flags&128))return Ms=!1,tc(e,t,n);Ms=!!(e.flags&131072)}else Ms=!1,_a&&t.flags&1048576&&fa(t,aa,t.index);switch(t.lanes=0,t.tag){case 2:var i=t.type;$s(e,t),e=t.pendingProps;var a=Ui(t,Bi.current);Ha(t,n),a=ko(null,t,i,e,a,n);var o=Ao();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,Wi(i)?(o=!0,Ji(t)):o=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,Ya(t),a.updater=_s,t.stateNode=a,a._reactInternals=t,xs(t,i,e,n),t=Vs(null,t,i,!0,o,n)):(t.tag=0,_a&&o&&pa(t),Ns(null,t,a,n),t=t.child),t;case 16:i=t.elementType;a:{switch($s(e,t),e=t.pendingProps,a=i._init,i=a(i._payload),t.type=i,a=t.tag=Jl(i),e=hs(i,e),a){case 0:t=zs(null,t,i,e,n);break a;case 1:t=Bs(null,t,i,e,n);break a;case 11:t=Ps(null,t,i,e,n);break a;case 14:t=Fs(null,t,i,hs(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:hs(i,a),zs(e,t,i,a,n);case 1:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),Bs(e,t,i,a,n);case 3:a:{if(Hs(t),e===null)throw Error(r(387));i=t.pendingProps,o=t.memoizedState,a=o.element,Xa(e,t),to(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=Ss(Error(r(423)),t),t=Us(e,t,i,n,a);break a}else if(i!==a){a=Ss(Error(r(424)),t),t=Us(e,t,i,n,a);break a}else for(ga=xi(t.stateNode.containerInfo.firstChild),ha=t,_a=!0,va=null,n=Pa(t,null,i,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Ea(),i===a){t=ec(e,t,n);break a}Ns(e,t,i,n)}t=t.child}return t;case 5:return uo(t),e===null&&Sa(t),i=t.type,a=t.pendingProps,o=e===null?null:e.memoizedProps,s=a.children,mi(i,a)?s=null:o!==null&&mi(i,o)&&(t.flags|=32),Rs(e,t),Ns(e,t,s,n),t.child;case 6:return e===null&&Sa(t),null;case 13:return Ks(e,t,n);case 4:return co(t,t.stateNode.containerInfo),i=t.pendingProps,e===null?t.child=Na(t,null,i,n):Ns(e,t,i,n),t.child;case 11:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),Ps(e,t,i,a,n);case 7:return Ns(e,t,t.pendingProps,n),t.child;case 8:return Ns(e,t,t.pendingProps.children,n),t.child;case 12:return Ns(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,Ri(Fa,i._currentValue),i._currentValue=s,o!==null)if(Z(o.value,s)){if(o.children===a.children&&!Vi.current){t=ec(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=Za(-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),Va(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),Va(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}Ns(e,t,a.children,n),t=t.child}return t;case 9:return a=t.type,i=t.pendingProps.children,Ha(t,n),a=Ua(a),i=i(a),t.flags|=1,Ns(e,t,i,n),t.child;case 14:return i=t.type,a=hs(i,t.pendingProps),a=hs(i.type,a),Fs(e,t,i,a,n);case 15:return Is(e,t,t.type,t.pendingProps,n);case 17:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:hs(i,a),$s(e,t),t.tag=1,Wi(i)?(e=!0,Ji(t)):e=!1,Ha(t,n),ys(t,i,a),xs(t,i,a,n),Vs(null,t,i,!0,e,n);case 19:return Qs(e,t,n);case 22:return Ls(e,t,n)}throw Error(r(156,t.tag))};function Wl(e,t){return ot(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 I: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=I,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=Ot(0),this.expirationTimes=Ot(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ot(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},Ya(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}};async function y(e,t={}){let n=typeof FormData<`u`&&t.body instanceof FormData,r=await fetch(e,{credentials:`same-origin`,headers:n?t.headers:{"Content-Type":`application/json`,...t.headers??{}},...t}),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 b(e){return e instanceof Error?e.message:String(e)}var x={session:()=>y(`/api/session`),login:e=>y(`/api/login`,{method:`POST`,body:JSON.stringify({secret:e})}),logout:()=>y(`/api/logout`,{method:`POST`,body:`{}`}),accounts:e=>y(`/api/accounts?${e.toString()}`),account:e=>y(`/api/accounts/${e}`),channels:e=>y(`/api/channels?${e.toString()}`),channel:e=>y(`/api/channels/${e}`),bots:e=>y(`/api/bots?${e.toString()}`),bot:e=>y(`/api/bots/${e}`),emoji:e=>y(`/api/emoji?${e.toString()}`),emojiAnimation:e=>y(`/api/emoji/${encodeURIComponent(e)}/animation`),messages:e=>y(`/api/messages?${e.toString()}`),message:(e,t)=>y(`/api/messages/detail?${new URLSearchParams({owner_user_id:String(e),msg_id:String(t)}).toString()}`),groupMessages:e=>y(`/api/messages/groups?${e.toString()}`),groupMessage:(e,t)=>y(`/api/messages/groups/detail?${new URLSearchParams({channel_id:String(e),msg_id:String(t)}).toString()}`),moderationCases:e=>y(`/api/moderation/cases?${e.toString()}`),moderationCase:e=>y(`/api/moderation/cases/${e}`),moderationReport:e=>y(`/api/moderation/reports/${e}`),claimModerationCase:(e,t)=>y(`/api/moderation/cases/${e}/claim`,{method:`POST`,body:JSON.stringify({expected_version:t})}),decideModerationCase:(e,t)=>y(`/api/moderation/cases/${e}/decide`,{method:`POST`,body:JSON.stringify(t)}),reviewModerationAppeal:(e,t,n)=>y(`/api/moderation/cases/${e}/appeals/${t}/review`,{method:`POST`,body:JSON.stringify(n)}),gifts:()=>y(`/api/gifts`),officialGifts:()=>y(`/api/official-gifts`),officialGiftAnimation:e=>y(`/api/official-gifts/${encodeURIComponent(e)}/animation`),giftAnimation:e=>y(`/api/gifts/${encodeURIComponent(e)}/animation`),giftCollectibles:e=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles`),giftCollectibleAnimation:(e,t,n)=>y(`/api/gifts/${encodeURIComponent(e)}/collectibles/${t}/${encodeURIComponent(n)}/animation`),importGift:e=>y(`/api/actions/import-gift`,{method:`POST`,body:e}),importOfficialGift:e=>y(`/api/actions/import-official-gift`,{method:`POST`,body:JSON.stringify(e)}),publishGiftCollectibles:(e,t)=>y(`/api/actions/publish-gift-collectibles?gift_id=${encodeURIComponent(e)}`,{method:`POST`,body:t}),action:(e,t)=>y(e,{method:`POST`,body:JSON.stringify(t)})},S=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),C=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),w={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`},T=(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,...w,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:C(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),E=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(T,{ref:i,iconNode:t,className:C(`lucide-${S(e)}`,n),...r}));return n.displayName=`${e}`,n},D=E(`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`}]]),O=E(`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`}]]),k=E(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),A=E(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),j=E(`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`}]]),M=E(`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`}]]),N=E(`ArrowLeft`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]),P=E(`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`}]]),F=E(`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`}]]),I=E(`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`}]]),L=E(`Check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),ee=E(`ChevronDown`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),R=E(`ChevronRight`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),te=E(`Clock3`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`polyline`,{points:`12 6 12 12 16.5 12`,key:`1aq6pp`}]]),ne=E(`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`}]]),re=E(`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`}]]),ie=E(`FileJson2`,[[`path`,{d:`M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4`,key:`1pf5j1`}],[`path`,{d:`M14 2v4a2 2 0 0 0 2 2h4`,key:`tnqrlb`}],[`path`,{d:`M4 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:`fq0c9t`}],[`path`,{d:`M8 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:`4gibmv`}]]),ae=E(`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`}]]),oe=E(`Gem`,[[`path`,{d:`M6 3h12l4 6-10 13L2 9Z`,key:`1pcd5k`}],[`path`,{d:`M11 3 8 9l4 13 4-13-3-6`,key:`1fcu3u`}],[`path`,{d:`M2 9h20`,key:`16fsjt`}]]),se=E(`Gift`,[[`rect`,{x:`3`,y:`8`,width:`18`,height:`4`,rx:`1`,key:`bkv52`}],[`path`,{d:`M12 8v13`,key:`1c76mn`}],[`path`,{d:`M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7`,key:`6wjy6b`}],[`path`,{d:`M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5`,key:`1ihvrl`}]]),ce=E(`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`}]]),le=E(`KeyRound`,[[`path`,{d:`M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z`,key:`1s6t7t`}],[`circle`,{cx:`16.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`w0ekpg`}]]),ue=E(`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`}]]),z=E(`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`}]]),de=E(`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`}]]),B=E(`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`}]]),fe=E(`Moon`,[[`path`,{d:`M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z`,key:`a7tn18`}]]),pe=E(`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`}]]),me=E(`Pause`,[[`rect`,{x:`14`,y:`4`,width:`4`,height:`16`,rx:`1`,key:`zuxfzm`}],[`rect`,{x:`6`,y:`4`,width:`4`,height:`16`,rx:`1`,key:`1okwgv`}]]),he=E(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),ge=E(`Plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),_e=E(`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`}]]),ve=E(`Search`,[[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}],[`path`,{d:`m21 21-4.3-4.3`,key:`1qie3q`}]]),ye=E(`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`}]]),be=E(`Server`,[[`rect`,{width:`20`,height:`8`,x:`2`,y:`2`,rx:`2`,ry:`2`,key:`ngkwjq`}],[`rect`,{width:`20`,height:`8`,x:`2`,y:`14`,rx:`2`,ry:`2`,key:`iecqi9`}],[`line`,{x1:`6`,x2:`6.01`,y1:`6`,y2:`6`,key:`16zg32`}],[`line`,{x1:`6`,x2:`6.01`,y1:`18`,y2:`18`,key:`nzw8ys`}]]),V=E(`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=E(`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`}]]),Se=E(`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`}]]),Ce=E(`Shield`,[[`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`}]]),we=E(`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`}]]),Te=E(`Star`,[[`path`,{d:`M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z`,key:`r04s7s`}]]),Ee=E(`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`}]]),De=E(`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`}]]),Oe=E(`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`}]]),ke=E(`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`}]]),Ae=E(`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`}]]),je=E(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),Me=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})),H=o(((e,t)=>{t.exports=Me()}))(),Ne=`telesrv.admin.lang`,Pe={en:{"app.adminConsole":`Admin Console`,"app.localAccess":`Local access`,"app.title":`telesrv admin`,"common.actions":`Actions`,"common.admins":`Admins`,"common.backToList":`Back to list`,"common.channel":`Channel`,"common.channelOrGroup":`Channel / Group`,"common.clear":`Clear`,"common.close":`Close`,"common.count":`Count`,"common.deleted":`Deleted`,"common.detail":`Details`,"common.device":`Device`,"common.disabled":`Disabled`,"common.enabled":`Enabled`,"common.fromPeer":`From Peer`,"common.group":`Group`,"common.id":`ID`,"common.limit":`Limit`,"common.loading":`Loading`,"common.member":`Member`,"common.members":`Members`,"common.messageId":`Message ID`,"common.name":`Name`,"common.no":`No`,"common.noResults":`No results`,"common.none":`None`,"common.normal":`Normal`,"common.operations":`Operations`,"common.owner":`Owner`,"common.platform":`Platform`,"common.refresh":`Refresh`,"common.search":`Search`,"common.sender":`Sender`,"common.status":`Status`,"common.survived":`Live`,"common.time":`Time`,"common.type":`Type`,"common.updatedAt":`Updated`,"common.username":`Username`,"common.valid":`Valid`,"common.verified":`Verified`,"common.views":`Views`,"common.yes":`Yes`,"route.accounts":`Accounts`,"route.accountsSubtitle":`Console / Accounts`,"route.channels":`Supergroups and Channels`,"route.channelsSubtitle":`Console / Channels`,"route.moderation":`Reports and Moderation`,"route.moderationSubtitle":`Console / Moderation`,"route.dashboard":`Operations Console`,"route.dashboardSubtitle":`Console / Overview`,"route.messages":`Message Audit`,"route.messagesSubtitle":`Console / Messages`,"route.gifts":`Star Gifts`,"route.giftsSubtitle":`Console / Star Gifts`,"route.giveGifts":`Give Gifts`,"route.giveGiftsSubtitle":`Console / Give Gifts`,"layout.navigation":`Navigation`,"layout.primaryNav":`Primary navigation`,"layout.dashboard":`Overview`,"layout.accounts":`Accounts`,"layout.channels":`Supergroups / Channels`,"layout.moderation":`Reports / Moderation`,"layout.messages":`Messages`,"layout.gifts":`Star Gifts`,"layout.giveGifts":`Give Gifts`,"layout.privateMessages":`Private`,"layout.groupMessages":`Groups`,"layout.runtime":`Runtime`,"layout.adminBackend":`Admin backend`,"layout.ready":`Ready`,"layout.pgRead":`PG read`,"layout.readOnly":`Read-only`,"layout.writeOps":`Write operations`,"layout.dryRun":`Dry-run`,"layout.actor":`Actor: {actor}`,"layout.logout":`Log out`,"language.en":`EN`,"language.zh":`中文`,"language.ru":`RU`,"theme.switchToDark":`Switch to dark theme`,"theme.switchToLight":`Switch to light theme`,"login.heading":`Operations Admin`,"login.body":`Enter credentials to open the console.`,"login.secret":`Admin password or token`,"login.submit":`Log in`,"login.submitting":`Logging in`,"dashboard.eyebrow":`Runtime Overview`,"dashboard.title":`Console Overview`,"dashboard.readPath":`Read path`,"dashboard.readPathValue":`PG read-only`,"dashboard.writePath":`Write path`,"dashboard.executionPolicy":`Execution policy`,"dashboard.dryRunFirst":`Dry-run first`,"dashboard.accountsText":`Account status, premium, verification, sessions.`,"dashboard.channelsText":`Public entities, member counts, verification state.`,"dashboard.messagesText":`Message boxes, updates, outbox state.`,"dashboard.strip.dryRun":`All dangerous actions start with dry-run`,"dashboard.strip.token":`Browser never stores internal tokens`,"dashboard.strip.pagination":`Lists use cursor pagination`,"dashboard.strip.snapshot":`Detail pages retain raw state snapshots`,"account.pageTitle":`Accounts`,"account.queryResults":`Search results`,"account.recentActive":`Recently active accounts`,"account.currentPage":`Accounts on page`,"account.onlineDevices":`Online device records`,"account.premium":`Premium`,"account.frozen":`Frozen`,"account.searchPlaceholder":`User ID / phone / username`,"account.userID":`User ID`,"account.phone":`Phone`,"account.lastActive":`Last active`,"account.notVerified":`Not verified`,"account.notPremium":`Not premium`,"account.premiumUntil":`Premium expires`,"account.starsBalance":`Stars balance`,"account.startingGrantApplied":`initial grant applied`,"account.startingGrantPending":`initial grant pending`,"account.activeSessions":`Authorized devices`,"account.accountFlags":`Account flags`,"account.restriction":`Restriction`,"account.restricted":`Restricted`,"account.createdAt":`Created`,"account.detailTitle":`Account #{id}`,"account.profile":`Account Profile`,"account.loadingDetail":`Loading account detail`,"account.waitingData":`Waiting for data`,"account.noUsername":`No username`,"account.noPhone":`No phone`,"account.accountFrozen":`Account frozen`,"account.accountActive":`Account active`,"account.authorizationsTitle":`Authorized Devices`,"account.authorizationsCount":`{count} authorizations`,"account.recentAdminOps":`Recent Admin Actions`,"account.recent30Audit":`Last 30 audit rows`,"account.actionDock":`Account Actions`,"account.freezeAccount":`Freeze account`,"account.updateFreeze":`Update freeze`,"account.unfreezeAccount":`Unfreeze account`,"account.freezeSince":`Frozen since`,"account.freezeUntil":`Appeal deadline`,"account.freezeUntilAria":`Freeze appeal deadline`,"account.freezeAppealURL":`Appeal URL`,"account.freezeAppealURLAria":`Freeze appeal URL`,"account.premiumMonths":`Premium duration (months)`,"account.premiumMonthsAria":`Set premium duration in months`,"account.setPremium":`Set premium`,"account.clearPremium":`Clear premium`,"account.starsAmount":`Stars to grant`,"account.starsAmountAria":`Set Stars amount to grant`,"account.grantStars":`Grant Stars`,"account.setVerified":`Set verified`,"account.clearVerified":`Clear verified`,"channel.pageTitle":`Supergroups and Channels`,"channel.recentUpdated":`Recently updated`,"channel.currentPage":`Entities on page`,"channel.megagroups":`Supergroups`,"channel.broadcasts":`Channels`,"channel.verifiedCount":`Verified`,"channel.searchPlaceholder":`Channel ID / username / title`,"channel.channelID":`Channel ID`,"channel.kind":`Kind`,"channel.title":`Title`,"channel.pts":`PTS`,"channel.detailProfile":`Channel Profile`,"channel.loadingDetail":`Loading channel detail`,"channel.creator":`Creator {id}`,"channel.governance":`Moderation`,"channel.governanceValue":`Banned {banned} / Kicked {kicked}`,"channel.flags":`Channel flags`,"channel.rawRow":`Channel Raw Row`,"channel.rawRowText":`Database read-only snapshot`,"channel.actionDock":`Channel Actions`,"channel.setVerified":`Set verified`,"channel.clearVerified":`Clear verified`,"channel.kind.broadcast":`Channel`,"channel.kind.forum":`Supergroup / Forum`,"channel.kind.megagroup":`Supergroup`,"channel.kind.generic":`Channel / Group`,"route.bots":`Bots`,"route.botsSubtitle":`Console / Bots`,"layout.bots":`Bots`,"bots.pageTitle":`Bots`,"bots.queryResults":`Search results`,"bots.recent":`Recently created bots`,"bots.currentPage":`Bots on page`,"bots.banned":`Banned`,"bots.active":`Active`,"bots.createTitle":`Create a system bot`,"bots.createHint":`Provision a bot account owned by the given user. The token is shown once after confirmation.`,"bots.ownerUserID":`Owner user ID`,"bots.name":`Display name`,"bots.namePlaceholder":`e.g. Service Bot`,"bots.username":`Username`,"bots.usernameHint":`Username must be 5-32 characters and end with 'bot'.`,"bots.create":`Create bot`,"bots.searchPlaceholder":`Bot ID / username`,"bots.botID":`Bot ID`,"bots.owner":`Owner`,"bots.status":`Status`,"bots.detailTitle":`Bot #{id}`,"bots.profile":`Bot Profile`,"bots.loadingDetail":`Loading bot detail`,"bots.unnamed":`Unnamed bot`,"bots.restriction":`Restriction`,"bots.actionDock":`Bot Actions`,"bots.banUntil":`Ban until`,"bots.ban":`Ban bot`,"bots.updateBan":`Update ban`,"bots.unban":`Unban bot`,"bots.type":`Type`,"bots.system":`System`,"bots.user":`User`,"bots.delete":`Delete bot`,"bots.deleteHint":`Permanently deletes this user-created bot and invalidates its token. This cannot be undone.`,"bots.systemHint":`System bots are built in and cannot be deleted.`,"flags.scam":`SCAM`,"flags.fake":`FAKE`,"flags.setScam":`Mark as SCAM`,"flags.clearScam":`Clear SCAM`,"flags.setFake":`Mark as FAKE`,"flags.clearFake":`Clear FAKE`,"attr.attributes":`Attributes`,"attr.settings":`Settings`,"attr.username":`Username`,"attr.setUsername":`Set username`,"attr.setSupport":`Mark as support`,"attr.clearSupport":`Clear support`,"attr.forProfile":`Profile color`,"attr.hasColor":`Enable color`,"attr.colorIndex":`Color index`,"attr.bgEmojiID":`Background emoji ID`,"attr.setColor":`Set color`,"attr.emojiDocID":`Emoji document ID`,"attr.emojiUntil":`Until (unix, 0 = permanent)`,"attr.setEmojiStatus":`Set emoji status`,"attr.gigagroup":`Gigagroup`,"attr.antispam":`Aggressive anti-spam`,"attr.participantsHidden":`Hide members`,"attr.noforwards":`Restrict forwarding`,"attr.joinToSend":`Join to send messages`,"attr.joinRequest":`Join by request`,"attr.slowmode":`Slowmode (seconds)`,"attr.applySettings":`Apply settings`,"route.emoji":`Emoji`,"route.emojiSubtitle":`Console / Emoji`,"layout.emoji":`Emoji`,"emoji.pageTitle":`Custom Emoji`,"emoji.queryResults":`Search results`,"emoji.recent":`Custom emoji catalog`,"emoji.currentPage":`Emoji on page`,"emoji.searchPlaceholder":`Document ID or emoji`,"emoji.copyID":`Copy document ID`,"emoji.noSet":`No set`,"emoji.hint":`Document IDs here can be pasted into the Emoji status field on account, bot and channel profiles.`,"messages.privateTitle":`Private Messages`,"messages.privateEyebrow":`Private message boxes`,"messages.groupTitle":`Group Messages`,"messages.groupEyebrow":`Supergroup / channel messages`,"messages.selectPrivatePeers":`Search and select the owner user and peer user first`,"messages.selectChannel":`Search and select a supergroup or channel first`,"messages.ownerUser":`Owner user`,"messages.peerUser":`Peer user`,"messages.beforeDatePlaceholder":`before_date cursor`,"messages.beforeIDPlaceholder":`before_msg_id cursor`,"messages.limitPlaceholder":`limit <= 100`,"messages.searchMessages":`Search messages`,"messages.nextPage":`Next page`,"messages.currentPage":`Messages on page`,"messages.deleted":`Deleted`,"messages.outgoing":`Outgoing`,"messages.incoming":`Incoming`,"messages.ownerPeer":`Owner / Peer`,"messages.deleteSelected":`Delete selected messages`,"messages.idsPlaceholder":`Message IDs, comma separated`,"messages.revoke":`Revoke for both sides`,"messages.previewDelete":`Dry-run delete`,"messages.clearHistory":`Clear private history`,"messages.maxIDPlaceholder":`max_id cutoff`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Clear only this side`,"messages.previewClearHistory":`Dry-run clear history`,"messages.direction":`Direction`,"messages.body":`Body`,"messages.privateDetailTitle":`Message #{id}`,"messages.detailEyebrow":`Message Detail`,"messages.backPrivate":`Back to private messages`,"messages.backGroup":`Back to group messages`,"messages.ownerPeerTitle":`Owner {owner} · Peer {peer}`,"messages.senderSubtitle":`Sender {sender} · {date}`,"messages.boxID":`Message box ID`,"messages.privateMessageID":`Private message ID`,"messages.messageSender":`Message sender`,"messages.messageBox":`Message Box`,"messages.dialogRow":`Dialog Row`,"messages.privateRow":`Private Message Row`,"messages.channelMessageRow":`Channel Message Row`,"messages.channelRow":`Channel Row`,"messages.userUpdateEvents":`Update Events`,"messages.channelUpdateEvents":`Channel Update Events`,"messages.eventJson":`Event JSON`,"messages.dispatchOutbox":`Dispatch Queue`,"messages.messageBoxesSnapshot":`message_boxes read-only snapshot`,"messages.dialogSnapshot":`dialogs read-only snapshot`,"messages.privateSnapshot":`private_messages read-only snapshot`,"messages.channelMessagesSnapshot":`channel_messages read-only snapshot`,"messages.channelSnapshot":`channels read-only snapshot`,"messages.userEventsSource":`durable user_update_events`,"messages.channelEventsSource":`durable channel_update_events`,"messages.outboxSource":`online/offline dispatch_outbox`,"messages.attempts":`Attempts`,"messages.deleteThis":`Delete this message`,"messages.groupDetailTitle":`Group Message #{id}`,"messages.channelGroupTitle":`Channel / Group {id}`,"messages.mediaCount":`With media`,"messages.channelPosts":`Channel posts`,"messages.channelGroup":`Channel / Group`,"messages.pinned":`Pinned`,"messages.channelPost":`Channel post`,"gifts.pageTitle":`Star Gift Catalog`,"giveGift.action":`Give`,"giveGift.eyebrow":`Grant a gift · no charge`,"giveGift.title":`Give gift`,"giveGift.recipientKind":`Recipient type`,"giveGift.recipientUser":`User`,"giveGift.recipientChannel":`Channel`,"giveGift.pickUser":`Recipient user`,"giveGift.pickChannel":`Recipient channel`,"giveGift.recipientRequired":`Select a recipient first`,"giveGift.sender":`Sender account ID`,"giveGift.senderHint":`Gifts are always sent from the system account 777000 (Telesrv).`,"giveGift.message":`Attached message (optional)`,"giveGift.messagePlaceholder":`Shown with the gift`,"giveGift.hideName":`Hide sender name from recipient`,"giveGift.upgrade":`Deliver as upgraded collectible`,"giveGift.upgradeNote":`The gift is minted as a unique collectible. Pick specific attributes below, or leave them on Random to draw from the published pool. The collectible number is assigned automatically. Requires a published collectible upgrade with remaining supply.`,"giveGift.model":`Model`,"giveGift.pattern":`Pattern`,"giveGift.backdrop":`Backdrop`,"giveGift.random":`Random`,"giveGift.confirm":`Give gift`,"giveGifts.pageTitle":`Give Gifts`,"giveGifts.eyebrow":`Grant catalog gifts to any user or channel`,"giveGifts.available":`Available gifts`,"giveGifts.sender":`Default sender`,"giveGifts.searchPlaceholder":`Search by title or gift ID`,"giveGifts.hint":`Pick a gift to grant. Delivery is free of charge and sent from the system account 777000 (Telesrv) by default.`,"giveGifts.pickGift":`Select a gift`,"giveGifts.selectPrompt":`Select a gift from the list to start.`,"gifts.eyebrow":`Catalog, immutable revisions and animation assets`,"gifts.total":`Catalog entries`,"gifts.enabled":`Enabled`,"gifts.received":`Received gifts`,"gifts.formats":`Accepted formats`,"gifts.add":`Add gift`,"gifts.searchPlaceholder":`Search gift ID, title or format`,"gifts.listSummary":`Showing {shown} of {total}`,"gifts.idRevision":`ID / Revision`,"gifts.price":`Price / Conversion`,"gifts.importTitle":`Import a Star Gift`,"gifts.importEyebrow":`Gift catalog operation`,"gifts.newRevision":`Create revision for gift #{id}`,"gifts.importHint":`Upload TGS or plain Lottie JSON. Lottie is normalized and compressed to TGS.`,"gifts.officialSource":`Official snapshot`,"gifts.fileSource":`Upload file`,"gifts.officialHint":`Choose a verified gift from data/official-gifts. Complete collectible pools are imported atomically.`,"gifts.officialSearch":`Search official gift ID or title`,"gifts.officialSelect":`Choose an official gift`,"gifts.officialRequired":`Choose an official gift first`,"gifts.officialResults":`Showing {shown} of {total}`,"gifts.officialCategoryLabel":`Official gift capability category`,"gifts.officialCategory.all":`All`,"gifts.officialCategory.upgrade":`Upgradable`,"gifts.officialCategory.craft":`Craftable`,"gifts.officialCategory.basic":`Not upgradable`,"gifts.officialUnnamed":`Unnamed official gift #{id}`,"gifts.officialAttributes":`{count} attributes`,"gifts.canUpgrade":`Can upgrade`,"gifts.cannotUpgrade":`Cannot upgrade`,"gifts.canCraft":`Can Craft`,"gifts.cannotCraft":`Cannot Craft`,"gifts.officialEmpty":`No official gifts match this category and search.`,"gifts.includeCollectible":`Import the complete collectible pool, including crafted models`,"gifts.animation":`Animation file`,"gifts.filePrompt":`Drop or choose a TGS / Lottie file`,"gifts.fileHint":`TGS, JSON or Lottie · validated before import`,"gifts.chooseFile":`Choose file`,"gifts.changeFile":`Change file`,"gifts.title":`Display title`,"gifts.titlePlaceholder":`e.g. Celebration Star`,"gifts.stars":`Price in Stars`,"gifts.convertStars":`Conversion Stars`,"gifts.sortOrder":`Sort order`,"gifts.reason":`Audit reason`,"gifts.reasonPlaceholder":`Briefly describe why this gift is being imported`,"gifts.enableAfterImport":`Enable after import`,"gifts.validate":`Dry-run validation`,"gifts.confirmImport":`Confirm import`,"gifts.stepDetails":`File and details`,"gifts.stepValidate":`Dry-run validation`,"gifts.stepImport":`Confirm import`,"gifts.fileRequired":`Choose a TGS or Lottie file first`,"gifts.source":`Source`,"gifts.replace":`New revision`,"gifts.disable":`Disable`,"gifts.enable":`Enable`,"gifts.empty":`No Star Gifts have been imported.`,"gifts.emptyHint":`Import the first animation above to build the gift catalog.`,"gifts.validationReady":`Validation passed`,"gifts.validationHint":`Review the normalized metadata, then confirm the import.`,"gifts.confirmState":`Apply the validated state change to gift #{id}?`,"collectibles.manage":`Attribute pool`,"collectibles.title":`Collectible pool · Gift #{id}`,"collectibles.eyebrow":`Unique gift attributes`,"collectibles.activeRevision":`Published revision {revision}`,"collectibles.published":`Published`,"collectibles.noPool":`No collectible pool published`,"collectibles.noPoolHint":`Publish models, patterns and backdrops to enable upgrades.`,"collectibles.publishNew":`Publish a new immutable revision`,"collectibles.immutableHint":`Dry-run checks every file and the attribute-pool structure before the revision becomes active.`,"collectibles.upgradeStars":`Upgrade price in Stars`,"collectibles.supply":`Unique supply`,"collectibles.slug":`Public slug prefix`,"collectibles.models":`Models`,"collectibles.patterns":`Patterns`,"collectibles.backdrops":`Backdrops`,"collectibles.model":`Model`,"collectibles.pattern":`Pattern`,"collectibles.backdrop":`Backdrop`,"collectibles.rarity":`Rarity ‰`,"collectibles.rarityHint":`Keep at least two attributes. Permille values are relative regular-upgrade weights; add/remove redistributes them to 1000.`,"collectibles.minimumAttributes":`Models, patterns, and backdrops must each contain at least two attributes.`,"collectibles.duplicateBackdropID":`Backdrop IDs must be unique within the pool.`,"collectibles.colorHint":`Colors are stored as 24-bit RGB values.`,"collectibles.addAttribute":`Add`,"collectibles.remove":`Remove attribute`,"collectibles.fileRequired":`Every model and pattern needs a TGS or Lottie file.`,"collectibles.backdropID":`Backdrop ID`,"collectibles.color.center":`Center`,"collectibles.color.edge":`Edge`,"collectibles.color.pattern":`Pattern`,"collectibles.color.text":`Text`,"collectibles.validationReady":`Attribute pool is valid`,"collectibles.validationHint":`Review the normalized assets, then publish this immutable revision.`,"collectibles.publish":`Publish revision`,"moderation.casesEyebrow":`Moderation / Cases`,"moderation.currentQueue":`Current queue`,"moderation.criticalCases":`Critical cases`,"moderation.pendingOrFailed":`Pending / failed actions`,"moderation.statusFilter":`Case status filter`,"moderation.statusFilter.active":`Active queue`,"moderation.statusFilter.all":`All statuses`,"moderation.assignee":`Reviewer`,"moderation.allAssignees":`Leave blank for all`,"moderation.case":`Case`,"moderation.target":`Target`,"moderation.severity":`Severity`,"moderation.reportsAndReporters":`Reports / Reporters`,"moderation.latestReport":`Latest report`,"moderation.review":`Review`,"moderation.status.open":`Open`,"moderation.status.in_review":`In review`,"moderation.status.action_pending":`Action pending`,"moderation.status.action_failed":`Action failed`,"moderation.status.resolved":`Resolved`,"moderation.status.dismissed":`Dismissed`,"moderation.status.appeal_review":`Appeal review`,"moderation.severity.low":`Low`,"moderation.severity.medium":`Medium`,"moderation.severity.high":`High`,"moderation.severity.critical":`Critical`,"moderation.targetType.user":`Account`,"moderation.targetType.chat":`Group`,"moderation.targetType.channel":`Channel`,"moderation.caseDetailTitle":`Review case #{id}`,"moderation.caseDetailEyebrow":`Moderation / Case detail`,"moderation.backToQueue":`Back to queue`,"moderation.loadingCase":`Loading moderation case…`,"moderation.versionAndUpdated":`Version {version} · Updated {time}`,"moderation.reportCount":`Reports`,"moderation.reportCountValue":`{reports} reports from {reporters} reporters`,"moderation.firstAndLatestReport":`First / latest report`,"moderation.evidence":`Report evidence`,"moderation.evidenceHint":`Shows up to the latest 100 reports; snapshots are frozen when reports are admitted.`,"moderation.sourceAndReason":`Source / Reason`,"moderation.reporter":`Reporter`,"moderation.option":`Option`,"moderation.decisionAudit":`Decision and action audit`,"moderation.decisionAuditHint":`Actions run idempotently through a lease worker; failures retain their error and attempt count.`,"moderation.appeals":`Appeals`,"moderation.caseActions":`Case actions`,"moderation.renewClaim":`Renew claim`,"moderation.claimCase":`Claim case`,"moderation.reviewReason":`Review reason`,"moderation.decisionPreset":`Decision template`,"moderation.preset.noViolation":`No violation (dismiss report)`,"moderation.preset.scam":`Mark as SCAM`,"moderation.preset.fake":`Mark as FAKE`,"moderation.preset.freeze":`Freeze account`,"moderation.preset.scamFreeze":`SCAM + freeze`,"moderation.preset.fakeFreeze":`FAKE + freeze`,"moderation.preset.deleteMessages":`Delete messages covered by evidence`,"moderation.preset.deleteAccount":`Delete account`,"moderation.evidenceMessageIDs":`Evidence message IDs (comma-separated)`,"moderation.privateOwnerUserID":`Private-chat owner_user_id`,"moderation.revokeForBoth":`Revoke for both sides`,"moderation.evidenceValidationHint":`The server will verify again that every message ID exists in this case's immutable report evidence.`,"moderation.failedActionHint":`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.`,"moderation.retryAction":`Retry action`,"moderation.submitDecision":`Submit decision`,"moderation.appealReviewTitle":`Appeal review #{id}`,"moderation.automaticRemedy":`Automatic remedy after approval`,"moderation.irreversibleAppealHint":`The case contains a completed irreversible deletion. It cannot be marked as approved and restored; deny it or escalate for manual handling.`,"moderation.denyAppeal":`Deny appeal`,"moderation.grantAppeal":`Grant appeal`,"moderation.reasonRequired":`A review reason is required.`,"moderation.appealReasonRequired":`An appeal review reason is required.`,"moderation.privateDeleteValidation":`Private-message deletion requires valid evidence message IDs and the reporter's owner_user_id.`,"moderation.channelDeleteValidation":`Channel-message deletion requires at least one valid evidence message ID.`,"moderation.confirmDecision":`Submit the “{decision}” decision? The action will run through the durable action queue.`,"moderation.confirmGrantAppeal":`Grant this appeal?`,"moderation.confirmDenyAppeal":`Deny this appeal?`,"moderation.remedy.clearFlags":`Clear SCAM / FAKE`,"moderation.remedy.unfreeze":`Unfreeze account`,"moderation.remedy.none":`No recovery action needed`,"moderation.source.account_peer":`Account / peer`,"moderation.source.profile_photo":`Profile photo`,"moderation.source.messages_spam":`Message spam`,"moderation.source.messages":`Messages`,"moderation.source.encrypted_spam":`Encrypted-chat spam`,"moderation.source.reaction":`Reaction`,"moderation.source.channel_spam":`Channel spam`,"moderation.source.story":`Story`,"moderation.source.ephemeral":`Ephemeral media`,"moderation.source.sponsored":`Sponsored message`,"moderation.source.antispam_false_positive":`Anti-spam false positive`,"moderation.reason.spam":`Spam`,"moderation.reason.violence":`Violence`,"moderation.reason.pornography":`Pornography`,"moderation.reason.child_abuse":`Child abuse`,"moderation.reason.other":`Other`,"moderation.reason.copyright":`Copyright`,"moderation.reason.geo_irrelevant":`Location-irrelevant`,"moderation.reason.fake":`Fake`,"moderation.reason.illegal_drugs":`Illegal drugs`,"moderation.reason.personal_details":`Personal details`,"messages.msgIDsInvalid":`Message IDs are invalid`,"auth.device":`Device`,"auth.platform":`Platform`,"auth.ip":`IP`,"auth.lastActive":`Last active`,"auth.revokeCurrent":`Revoke current`,"auth.keepCurrent":`Keep current`,"auth.revokeAll":`Revoke all devices`,"picker.userPlaceholder":`Search user_id / phone / username`,"picker.channelPlaceholder":`Search channel_id / username / title`,"picker.verified":`Verified`,"picker.regular":`Regular`,"action.reasonRequired":`Please enter an operation reason`,"action.flow":`Action Flow`,"action.close":`Close`,"action.stepReason":`Enter reason`,"action.stepDryRun":`Dry-run check`,"action.stepConfirm":`Confirm execution`,"action.reason":`Operation reason`,"action.reasonPlaceholder":`Describe why this operation is being performed`,"action.requestPreview":`Request preview`,"action.result":`Action result`,"action.commandID":`Command ID`,"action.status":`Status`,"action.dryRun":`Dry-run`,"action.runAgain":`Run dry-run again`,"action.runDry":`Run dry-run first`,"action.confirm":`Confirm execution`,"audit.id":`ID`,"audit.commandID":`Command ID`,"audit.action":`Action`,"audit.actor":`Actor`,"audit.status":`Status`,"audit.dryRun":`Dry-run`,"audit.reason":`Reason`,"audit.time":`Time`},zh:{"app.adminConsole":`管理控制台`,"app.localAccess":`本地访问`,"app.title":`telesrv 管理后台`,"common.actions":`操作`,"common.admins":`管理员`,"common.backToList":`返回列表`,"common.channel":`频道`,"common.channelOrGroup":`频道/群`,"common.clear":`清除`,"common.close":`关闭`,"common.count":`数量`,"common.deleted":`已删除`,"common.detail":`详情`,"common.device":`设备`,"common.disabled":`已禁用`,"common.enabled":`已启用`,"common.fromPeer":`From Peer`,"common.group":`群组`,"common.id":`ID`,"common.limit":`条数`,"common.loading":`加载中`,"common.member":`成员`,"common.members":`成员`,"common.messageId":`消息 ID`,"common.name":`姓名`,"common.no":`否`,"common.noResults":`无结果`,"common.none":`无`,"common.normal":`正常`,"common.operations":`操作`,"common.owner":`所属`,"common.platform":`平台`,"common.refresh":`刷新`,"common.search":`查询`,"common.sender":`发送方`,"common.status":`状态`,"common.survived":`存活`,"common.time":`时间`,"common.type":`类型`,"common.updatedAt":`更新时间`,"common.username":`用户名`,"common.valid":`有效`,"common.verified":`已认证`,"common.views":`浏览`,"common.yes":`是`,"route.accounts":`账号管理`,"route.accountsSubtitle":`控制台 / 账号`,"route.channels":`超级群与频道`,"route.channelsSubtitle":`控制台 / 频道`,"route.moderation":`举报与审核`,"route.moderationSubtitle":`控制台 / 内容安全`,"route.dashboard":`运维控制台`,"route.dashboardSubtitle":`控制台 / 总览`,"route.messages":`消息审计`,"route.messagesSubtitle":`控制台 / 消息`,"route.gifts":`星星礼物`,"route.giftsSubtitle":`控制台 / 星星礼物`,"route.giveGifts":`赠送礼物`,"route.giveGiftsSubtitle":`控制台 / 赠送礼物`,"layout.navigation":`导航`,"layout.primaryNav":`主导航`,"layout.dashboard":`总览`,"layout.accounts":`账号`,"layout.channels":`超级群/频道`,"layout.moderation":`举报/审核`,"layout.messages":`消息`,"layout.gifts":`礼物目录`,"layout.giveGifts":`赠送礼物`,"layout.privateMessages":`私聊`,"layout.groupMessages":`群聊`,"layout.runtime":`运行状态`,"layout.adminBackend":`管理后台`,"layout.ready":`就绪`,"layout.pgRead":`PG 读取`,"layout.readOnly":`只读`,"layout.writeOps":`写操作`,"layout.dryRun":`预演`,"layout.actor":`操作者:{actor}`,"layout.logout":`退出`,"language.en":`EN`,"language.zh":`中文`,"language.ru":`RU`,"theme.switchToDark":`切换到深色主题`,"theme.switchToLight":`切换到浅色主题`,"login.heading":`运维后台`,"login.body":`输入凭据后进入控制台。`,"login.secret":`管理员密码或 token`,"login.submit":`登录`,"login.submitting":`登录中`,"dashboard.eyebrow":`运行总览`,"dashboard.title":`控制台总览`,"dashboard.readPath":`读路径`,"dashboard.readPathValue":`PG 只读`,"dashboard.writePath":`写路径`,"dashboard.executionPolicy":`执行策略`,"dashboard.dryRunFirst":`先预演`,"dashboard.accountsText":`账号状态、会员、认证、会话。`,"dashboard.channelsText":`公开实体、成员计数、认证状态。`,"dashboard.messagesText":`消息盒、update、outbox 状态。`,"dashboard.strip.dryRun":`所有危险操作先预演`,"dashboard.strip.token":`浏览器不持有内部 token`,"dashboard.strip.pagination":`列表使用游标分页`,"dashboard.strip.snapshot":`详情页保留原始状态快照`,"account.pageTitle":`账号`,"account.queryResults":`查询结果`,"account.recentActive":`最近活跃账号`,"account.currentPage":`当前页账号`,"account.onlineDevices":`在线设备记录`,"account.premium":`会员`,"account.frozen":`冻结`,"account.searchPlaceholder":`用户 ID / 手机号 / 用户名`,"account.userID":`用户 ID`,"account.phone":`手机号`,"account.lastActive":`最近活跃`,"account.notVerified":`未认证`,"account.notPremium":`非会员`,"account.premiumUntil":`会员到期`,"account.starsBalance":`Stars 余额`,"account.startingGrantApplied":`初始赠送已发放`,"account.startingGrantPending":`初始赠送未触发`,"account.activeSessions":`授权设备`,"account.accountFlags":`账号标记`,"account.restriction":`限制状态`,"account.restricted":`已限制`,"account.createdAt":`创建时间`,"account.detailTitle":`账号 #{id}`,"account.profile":`账号档案`,"account.loadingDetail":`加载账号详情`,"account.waitingData":`等待数据`,"account.noUsername":`无用户名`,"account.noPhone":`无手机号`,"account.accountFrozen":`账号已冻结`,"account.accountActive":`账号正常`,"account.authorizationsTitle":`授权设备`,"account.authorizationsCount":`共 {count} 个授权`,"account.recentAdminOps":`最近后台操作`,"account.recent30Audit":`最近 30 条审计`,"account.actionDock":`账号操作`,"account.freezeAccount":`冻结账号`,"account.updateFreeze":`更新冻结信息`,"account.unfreezeAccount":`解冻账号`,"account.freezeSince":`冻结开始时间`,"account.freezeUntil":`申诉截止时间`,"account.freezeUntilAria":`账号冻结申诉截止时间`,"account.freezeAppealURL":`申诉链接`,"account.freezeAppealURLAria":`账号冻结申诉链接`,"account.premiumMonths":`会员时长(月)`,"account.premiumMonthsAria":`设置会员时长,单位月`,"account.setPremium":`设置会员`,"account.clearPremium":`取消会员`,"account.starsAmount":`赠送 Stars 数量`,"account.starsAmountAria":`设置要赠送的 Stars 数量`,"account.grantStars":`赠送 Stars`,"account.setVerified":`设置认证`,"account.clearVerified":`取消认证`,"channel.pageTitle":`超级群与频道`,"channel.recentUpdated":`最近更新`,"channel.currentPage":`当前页实体`,"channel.megagroups":`超级群`,"channel.broadcasts":`频道`,"channel.verifiedCount":`已认证`,"channel.searchPlaceholder":`频道 ID / 用户名 / 标题`,"channel.channelID":`频道 ID`,"channel.kind":`类型`,"channel.title":`标题`,"channel.pts":`PTS`,"channel.detailProfile":`频道档案`,"channel.loadingDetail":`加载频道详情`,"channel.creator":`创建者 {id}`,"channel.governance":`治理状态`,"channel.governanceValue":`封禁 {banned} / 踢出 {kicked}`,"channel.flags":`频道标记`,"channel.rawRow":`频道原始行`,"channel.rawRowText":`数据库只读快照`,"channel.actionDock":`频道操作`,"channel.setVerified":`设置认证`,"channel.clearVerified":`取消认证`,"channel.kind.broadcast":`频道`,"channel.kind.forum":`超级群/论坛`,"channel.kind.megagroup":`超级群`,"channel.kind.generic":`频道/群`,"route.bots":`机器人`,"route.botsSubtitle":`控制台 / 机器人`,"layout.bots":`机器人`,"bots.pageTitle":`机器人`,"bots.queryResults":`查询结果`,"bots.recent":`最近创建的机器人`,"bots.currentPage":`当前页机器人`,"bots.banned":`已封禁`,"bots.active":`正常`,"bots.createTitle":`创建系统机器人`,"bots.createHint":`为指定用户创建机器人账号。确认后 token 只显示一次。`,"bots.ownerUserID":`所属用户 ID`,"bots.name":`显示名称`,"bots.namePlaceholder":`例如:服务机器人`,"bots.username":`用户名`,"bots.usernameHint":`用户名需 5-32 个字符,且以 bot 结尾。`,"bots.create":`创建机器人`,"bots.searchPlaceholder":`机器人 ID / 用户名`,"bots.botID":`机器人 ID`,"bots.owner":`所属用户`,"bots.status":`状态`,"bots.detailTitle":`机器人 #{id}`,"bots.profile":`机器人档案`,"bots.loadingDetail":`加载机器人详情`,"bots.unnamed":`未命名机器人`,"bots.restriction":`限制状态`,"bots.actionDock":`机器人操作`,"bots.banUntil":`封禁至`,"bots.ban":`封禁机器人`,"bots.updateBan":`更新封禁`,"bots.unban":`解封机器人`,"bots.type":`类型`,"bots.system":`系统`,"bots.user":`用户`,"bots.delete":`删除机器人`,"bots.deleteHint":`永久删除该用户创建的机器人并使其 token 失效。此操作不可撤销。`,"bots.systemHint":`系统内置机器人不可删除。`,"flags.scam":`SCAM`,"flags.fake":`FAKE`,"flags.setScam":`标记为 SCAM`,"flags.clearScam":`移除 SCAM`,"flags.setFake":`标记为 FAKE`,"flags.clearFake":`移除 FAKE`,"attr.attributes":`属性`,"attr.settings":`设置`,"attr.username":`用户名`,"attr.setUsername":`设置用户名`,"attr.setSupport":`标记为客服`,"attr.clearSupport":`取消客服`,"attr.forProfile":`资料颜色`,"attr.hasColor":`启用颜色`,"attr.colorIndex":`颜色编号`,"attr.bgEmojiID":`背景 emoji ID`,"attr.setColor":`设置颜色`,"attr.emojiDocID":`Emoji 文档 ID`,"attr.emojiUntil":`有效期 (unix, 0 = 永久)`,"attr.setEmojiStatus":`设置 emoji 状态`,"attr.gigagroup":`广播群 (gigagroup)`,"attr.antispam":`激进反垃圾`,"attr.participantsHidden":`隐藏成员`,"attr.noforwards":`禁止转发`,"attr.joinToSend":`先加入才能发言`,"attr.joinRequest":`加入需审批`,"attr.slowmode":`慢速模式 (秒)`,"attr.applySettings":`应用设置`,"route.emoji":`Emoji`,"route.emojiSubtitle":`控制台 / Emoji`,"layout.emoji":`Emoji`,"emoji.pageTitle":`自定义 Emoji`,"emoji.queryResults":`查询结果`,"emoji.recent":`自定义 Emoji 目录`,"emoji.currentPage":`当前页 Emoji`,"emoji.searchPlaceholder":`文档 ID 或表情`,"emoji.copyID":`复制文档 ID`,"emoji.noSet":`无所属集合`,"emoji.hint":`这里的文档 ID 可直接填入账号、机器人和频道资料的 Emoji 状态字段。`,"messages.privateTitle":`私聊消息`,"messages.privateEyebrow":`私聊消息盒`,"messages.groupTitle":`群聊消息`,"messages.groupEyebrow":`超级群 / 频道消息`,"messages.selectPrivatePeers":`请先搜索并选择所属用户和对端用户`,"messages.selectChannel":`请先搜索并选择超级群或频道`,"messages.ownerUser":`所属用户`,"messages.peerUser":`对端用户`,"messages.beforeDatePlaceholder":`before_date 游标`,"messages.beforeIDPlaceholder":`before_msg_id 游标`,"messages.limitPlaceholder":`条数 <= 100`,"messages.searchMessages":`查询消息`,"messages.nextPage":`下一页`,"messages.currentPage":`当前页消息`,"messages.deleted":`已删除`,"messages.outgoing":`发出消息`,"messages.incoming":`收到`,"messages.ownerPeer":`所属 / 对端`,"messages.deleteSelected":`删除指定消息`,"messages.idsPlaceholder":`消息 ID,逗号分隔`,"messages.revoke":`同步撤回`,"messages.previewDelete":`预演删除`,"messages.clearHistory":`清空私聊历史`,"messages.maxIDPlaceholder":`max_id 截止消息`,"messages.maxBatchesPlaceholder":`max_batches 批次数`,"messages.justClear":`仅清本侧`,"messages.previewClearHistory":`预演清历史`,"messages.direction":`方向`,"messages.body":`正文`,"messages.privateDetailTitle":`消息 #{id}`,"messages.detailEyebrow":`消息详情`,"messages.backPrivate":`返回私聊消息`,"messages.backGroup":`返回群聊消息`,"messages.ownerPeerTitle":`所属 {owner} · 对端 {peer}`,"messages.senderSubtitle":`发送方 {sender} · {date}`,"messages.boxID":`消息盒 ID`,"messages.privateMessageID":`私聊消息 ID`,"messages.messageSender":`发送方`,"messages.messageBox":`消息盒`,"messages.dialogRow":`会话行`,"messages.privateRow":`私聊消息行`,"messages.channelMessageRow":`消息行`,"messages.channelRow":`频道行`,"messages.userUpdateEvents":`更新事件`,"messages.channelUpdateEvents":`频道更新事件`,"messages.eventJson":`事件 JSON`,"messages.dispatchOutbox":`分发队列`,"messages.messageBoxesSnapshot":`message_boxes 只读快照`,"messages.dialogSnapshot":`dialogs 只读快照`,"messages.privateSnapshot":`private_messages 只读快照`,"messages.channelMessagesSnapshot":`channel_messages 只读快照`,"messages.channelSnapshot":`channels 只读快照`,"messages.userEventsSource":`durable user_update_events`,"messages.channelEventsSource":`durable channel_update_events`,"messages.outboxSource":`在线/离线 dispatch_outbox`,"messages.attempts":`尝试`,"messages.deleteThis":`删除此消息`,"messages.groupDetailTitle":`群聊消息 #{id}`,"messages.channelGroupTitle":`频道/群 {id}`,"messages.mediaCount":`有媒体`,"messages.channelPosts":`频道帖子`,"messages.channelGroup":`频道 / 群`,"messages.pinned":`置顶`,"messages.channelPost":`频道帖子`,"gifts.pageTitle":`星星礼物目录`,"giveGift.action":`赠送`,"giveGift.eyebrow":`发放礼物 · 免费`,"giveGift.title":`赠送礼物`,"giveGift.recipientKind":`接收方类型`,"giveGift.recipientUser":`用户`,"giveGift.recipientChannel":`频道`,"giveGift.pickUser":`接收用户`,"giveGift.pickChannel":`接收频道`,"giveGift.recipientRequired":`请先选择接收方`,"giveGift.sender":`发送方账号 ID`,"giveGift.senderHint":`礼物始终由系统账号 777000(Telesrv)发送。`,"giveGift.message":`附加留言(可选)`,"giveGift.messagePlaceholder":`随礼物一起显示`,"giveGift.hideName":`对接收方隐藏发送方名称`,"giveGift.upgrade":`作为升级收藏品发放`,"giveGift.upgradeNote":`礼物将铸造为唯一收藏品。可在下方指定具体属性,或保持“随机”从已发布的属性池中抽取。编号自动分配。需要存在有剩余供应量的已发布收藏品升级。`,"giveGift.model":`模型`,"giveGift.pattern":`图案`,"giveGift.backdrop":`背景`,"giveGift.random":`随机`,"giveGift.confirm":`赠送礼物`,"giveGifts.pageTitle":`赠送礼物`,"giveGifts.eyebrow":`向任意用户或频道发放目录礼物`,"giveGifts.available":`可用礼物`,"giveGifts.sender":`默认发送方`,"giveGifts.searchPlaceholder":`按标题或礼物 ID 搜索`,"giveGifts.hint":`选择要发放的礼物。发放免费,默认由系统账号 777000(Telesrv)发送。`,"giveGifts.pickGift":`选择礼物`,"giveGifts.selectPrompt":`从列表中选择一个礼物开始。`,"gifts.eyebrow":`目录、不可变版本与动画资源`,"gifts.total":`目录条目`,"gifts.enabled":`已启用`,"gifts.received":`已领取礼物`,"gifts.formats":`支持格式`,"gifts.add":`添加礼物`,"gifts.searchPlaceholder":`搜索礼物 ID、标题或格式`,"gifts.listSummary":`显示 {shown} / {total} 项`,"gifts.idRevision":`ID / 版本`,"gifts.price":`售价 / 兑换`,"gifts.importTitle":`导入星星礼物`,"gifts.importEyebrow":`礼物目录操作`,"gifts.newRevision":`为礼物 #{id} 创建新版本`,"gifts.importHint":`支持 TGS 或纯 Lottie JSON;Lottie 会规范化并压缩成 TGS。`,"gifts.officialSource":`官方资源库`,"gifts.fileSource":`上传文件`,"gifts.officialHint":`从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。`,"gifts.officialSearch":`搜索官方礼物 ID 或标题`,"gifts.officialSelect":`请选择官方礼物`,"gifts.officialRequired":`请先选择一个官方礼物`,"gifts.officialResults":`显示 {shown} / {total} 项`,"gifts.officialCategoryLabel":`官方礼物能力分类`,"gifts.officialCategory.all":`全部`,"gifts.officialCategory.upgrade":`可升级`,"gifts.officialCategory.craft":`可 Craft`,"gifts.officialCategory.basic":`不可升级`,"gifts.officialUnnamed":`未命名官方礼物 #{id}`,"gifts.officialAttributes":`{count} 个属性`,"gifts.canUpgrade":`可升级`,"gifts.cannotUpgrade":`不可升级`,"gifts.canCraft":`可 Craft`,"gifts.cannotCraft":`不可 Craft`,"gifts.officialEmpty":`当前分类和搜索条件下没有官方礼物。`,"gifts.includeCollectible":`完整导入 collectible 属性池(包含 crafted 模型)`,"gifts.animation":`动画文件`,"gifts.filePrompt":`拖放或选择 TGS / Lottie 文件`,"gifts.fileHint":`支持 TGS、JSON、Lottie,导入前会先进行校验`,"gifts.chooseFile":`选择文件`,"gifts.changeFile":`更换文件`,"gifts.title":`显示标题`,"gifts.titlePlaceholder":`例如:庆典星星`,"gifts.stars":`售价 Stars`,"gifts.convertStars":`可兑换 Stars`,"gifts.sortOrder":`排序值`,"gifts.reason":`审计原因`,"gifts.reasonPlaceholder":`简要说明本次导入礼物的原因`,"gifts.enableAfterImport":`导入后启用`,"gifts.validate":`Dry-run 校验`,"gifts.confirmImport":`确认导入`,"gifts.stepDetails":`文件与信息`,"gifts.stepValidate":`Dry-run 校验`,"gifts.stepImport":`确认导入`,"gifts.fileRequired":`请先选择 TGS 或 Lottie 文件`,"gifts.source":`来源`,"gifts.replace":`创建新版本`,"gifts.disable":`停用`,"gifts.enable":`启用`,"gifts.empty":`尚未导入星星礼物。`,"gifts.emptyHint":`从上方导入第一个动画,开始搭建礼物目录。`,"gifts.validationReady":`校验已通过`,"gifts.validationHint":`确认规范化后的元数据无误,再执行正式导入。`,"gifts.confirmState":`确认执行礼物 #{id} 的状态变更吗?`,"collectibles.manage":`属性池`,"collectibles.title":`Collectibles 属性池 · 礼物 #{id}`,"collectibles.eyebrow":`唯一礼物属性管理`,"collectibles.activeRevision":`已发布版本 {revision}`,"collectibles.published":`已发布`,"collectibles.noPool":`尚未发布 Collectibles 属性池`,"collectibles.noPoolHint":`发布模型、图案与背景后,客户端即可升级为唯一礼物。`,"collectibles.publishNew":`发布新的不可变版本`,"collectibles.immutableHint":`Dry-run 会校验全部文件和属性池结构,通过后才切换为当前版本。`,"collectibles.upgradeStars":`升级价格 Stars`,"collectibles.supply":`唯一礼物总量`,"collectibles.slug":`公开 Slug 前缀`,"collectibles.models":`模型`,"collectibles.patterns":`图案`,"collectibles.backdrops":`背景`,"collectibles.model":`模型`,"collectibles.pattern":`图案`,"collectibles.backdrop":`背景`,"collectibles.rarity":`稀有度 ‰`,"collectibles.rarityHint":`每类至少保留两项。Permille 是普通升级的相对权重,增删时会自动重新分配为 1000。`,"collectibles.minimumAttributes":`模型、图案和背景每类都必须至少包含两个属性。`,"collectibles.duplicateBackdropID":`同一属性池中的背景 ID 必须唯一。`,"collectibles.colorHint":`颜色会按 24 位 RGB 数值保存。`,"collectibles.addAttribute":`添加`,"collectibles.remove":`删除属性`,"collectibles.fileRequired":`每个模型和图案都必须选择 TGS 或 Lottie 文件。`,"collectibles.backdropID":`背景 ID`,"collectibles.color.center":`中心色`,"collectibles.color.edge":`边缘色`,"collectibles.color.pattern":`图案色`,"collectibles.color.text":`文字色`,"collectibles.validationReady":`属性池校验通过`,"collectibles.validationHint":`确认规范化资源无误后,即可发布这个不可变版本。`,"collectibles.publish":`发布版本`,"moderation.casesEyebrow":`内容安全 / 案件`,"moderation.currentQueue":`当前队列`,"moderation.criticalCases":`关键案件`,"moderation.pendingOrFailed":`处置待完成 / 失败`,"moderation.statusFilter":`案件状态筛选`,"moderation.statusFilter.active":`活跃队列`,"moderation.statusFilter.all":`全部状态`,"moderation.assignee":`审核人`,"moderation.allAssignees":`留空为全部`,"moderation.case":`案件`,"moderation.target":`目标`,"moderation.severity":`等级`,"moderation.reportsAndReporters":`举报 / 举报人`,"moderation.latestReport":`最近举报`,"moderation.review":`审核`,"moderation.status.open":`待审核`,"moderation.status.in_review":`审核中`,"moderation.status.action_pending":`等待处置`,"moderation.status.action_failed":`处置失败`,"moderation.status.resolved":`已处置`,"moderation.status.dismissed":`已驳回`,"moderation.status.appeal_review":`申诉复核中`,"moderation.severity.low":`低`,"moderation.severity.medium":`中`,"moderation.severity.high":`高`,"moderation.severity.critical":`关键`,"moderation.targetType.user":`账号`,"moderation.targetType.chat":`群组`,"moderation.targetType.channel":`频道`,"moderation.caseDetailTitle":`审核案件 #{id}`,"moderation.caseDetailEyebrow":`内容安全 / 案件详情`,"moderation.backToQueue":`返回队列`,"moderation.loadingCase":`正在加载审核案件…`,"moderation.versionAndUpdated":`版本 {version} · 最近更新 {time}`,"moderation.reportCount":`举报数`,"moderation.reportCountValue":`{reports} 次({reporters} 位举报人)`,"moderation.firstAndLatestReport":`首个 / 最近举报`,"moderation.evidence":`举报证据`,"moderation.evidenceHint":`最多显示最近 100 条;快照在举报受理时冻结。`,"moderation.sourceAndReason":`来源 / 原因`,"moderation.reporter":`举报人`,"moderation.option":`选项`,"moderation.decisionAudit":`决定与处置审计`,"moderation.decisionAuditHint":`动作由租约 worker 幂等执行;失败保留错误与尝试次数。`,"moderation.appeals":`申诉`,"moderation.caseActions":`案件操作`,"moderation.renewClaim":`续领案件`,"moderation.claimCase":`领取案件`,"moderation.reviewReason":`审核理由`,"moderation.decisionPreset":`决定模板`,"moderation.preset.noViolation":`无违规(驳回举报)`,"moderation.preset.scam":`标记 SCAM`,"moderation.preset.fake":`标记 FAKE`,"moderation.preset.freeze":`冻结账号`,"moderation.preset.scamFreeze":`SCAM + 冻结`,"moderation.preset.fakeFreeze":`FAKE + 冻结`,"moderation.preset.deleteMessages":`删除证据覆盖的消息`,"moderation.preset.deleteAccount":`删除账号`,"moderation.evidenceMessageIDs":`证据消息 ID(逗号分隔)`,"moderation.privateOwnerUserID":`私聊 owner_user_id`,"moderation.revokeForBoth":`双方撤回`,"moderation.evidenceValidationHint":`服务端会再次校验每个消息 ID 必须存在于该案件的不可变举报证据中。`,"moderation.failedActionHint":`处置已部分执行,不能直接改为无违规;请选择新的处置动作重新执行并保留旧失败审计。`,"moderation.retryAction":`重新执行处置`,"moderation.submitDecision":`提交决定`,"moderation.appealReviewTitle":`申诉复核 #{id}`,"moderation.automaticRemedy":`通过后自动恢复`,"moderation.irreversibleAppealHint":`案件包含已成功的不可逆删除动作,不能标记为“申诉通过并已恢复”;请驳回或升级人工处理。`,"moderation.denyAppeal":`驳回申诉`,"moderation.grantAppeal":`通过申诉`,"moderation.reasonRequired":`必须填写审核理由。`,"moderation.appealReasonRequired":`必须填写申诉复核理由。`,"moderation.privateDeleteValidation":`私聊删除需要合法的证据消息 ID 和举报人 owner_user_id。`,"moderation.channelDeleteValidation":`频道删除需要至少一个合法的证据消息 ID。`,"moderation.confirmDecision":`确认提交“{decision}”决定?处置会通过 durable action 队列执行。`,"moderation.confirmGrantAppeal":`确认通过申诉?`,"moderation.confirmDenyAppeal":`确认驳回申诉?`,"moderation.remedy.clearFlags":`清除 SCAM / FAKE`,"moderation.remedy.unfreeze":`解除冻结`,"moderation.remedy.none":`无需恢复动作`,"moderation.source.account_peer":`账号 / Peer`,"moderation.source.profile_photo":`资料照片`,"moderation.source.messages_spam":`消息垃圾内容`,"moderation.source.messages":`消息`,"moderation.source.encrypted_spam":`加密聊天垃圾内容`,"moderation.source.reaction":`回应`,"moderation.source.channel_spam":`频道垃圾内容`,"moderation.source.story":`Story`,"moderation.source.ephemeral":`阅后即焚媒体`,"moderation.source.sponsored":`赞助消息`,"moderation.source.antispam_false_positive":`反垃圾误判`,"moderation.reason.spam":`垃圾内容`,"moderation.reason.violence":`暴力`,"moderation.reason.pornography":`色情内容`,"moderation.reason.child_abuse":`儿童虐待`,"moderation.reason.other":`其他`,"moderation.reason.copyright":`版权`,"moderation.reason.geo_irrelevant":`与地区无关`,"moderation.reason.fake":`虚假信息`,"moderation.reason.illegal_drugs":`非法药物`,"moderation.reason.personal_details":`个人信息`,"messages.msgIDsInvalid":`消息 ID 无效`,"auth.device":`设备`,"auth.platform":`平台`,"auth.ip":`IP`,"auth.lastActive":`最近活跃`,"auth.revokeCurrent":`撤销当前`,"auth.keepCurrent":`保留当前`,"auth.revokeAll":`撤销全部设备`,"picker.userPlaceholder":`搜索 user_id / phone / username`,"picker.channelPlaceholder":`搜索 channel_id / username / title`,"picker.verified":`认证`,"picker.regular":`普通`,"action.reasonRequired":`请填写操作原因`,"action.flow":`操作流程`,"action.close":`关闭`,"action.stepReason":`填写原因`,"action.stepDryRun":`预演检查`,"action.stepConfirm":`确认执行`,"action.reason":`操作原因`,"action.reasonPlaceholder":`说明本次操作原因`,"action.requestPreview":`请求预览`,"action.result":`操作结果`,"action.commandID":`命令 ID`,"action.status":`状态`,"action.dryRun":`预演`,"action.runAgain":`重新预演`,"action.runDry":`先预演`,"action.confirm":`确认执行`,"audit.id":`ID`,"audit.commandID":`命令 ID`,"audit.action":`动作`,"audit.actor":`操作者`,"audit.status":`状态`,"audit.dryRun":`预演`,"audit.reason":`原因`,"audit.time":`时间`},ru:{"app.adminConsole":`Панель администратора`,"app.localAccess":`Локальный доступ`,"app.title":`telesrv admin`,"common.actions":`Действия`,"common.admins":`Администраторы`,"common.backToList":`Назад к списку`,"common.channel":`Канал`,"common.channelOrGroup":`Канал / Группа`,"common.clear":`Очистить`,"common.close":`Закрыть`,"common.count":`Количество`,"common.deleted":`Удалено`,"common.detail":`Детали`,"common.device":`Устройство`,"common.disabled":`Отключено`,"common.enabled":`Включено`,"common.fromPeer":`От пира`,"common.group":`Группа`,"common.id":`ID`,"common.limit":`Лимит`,"common.loading":`Загрузка…`,"common.member":`Участник`,"common.members":`Участники`,"common.messageId":`ID сообщения`,"common.name":`Имя`,"common.no":`Нет`,"common.noResults":`Нет результатов`,"common.none":`Нет`,"common.normal":`Обычный`,"common.operations":`Операции`,"common.owner":`Владелец`,"common.platform":`Платформа`,"common.refresh":`Обновить`,"common.search":`Поиск`,"common.sender":`Отправитель`,"common.status":`Статус`,"common.survived":`Уцелело`,"common.time":`Время`,"common.type":`Тип`,"common.updatedAt":`Обновлено`,"common.username":`Имя пользователя`,"common.valid":`Действителен`,"common.verified":`Подтверждён`,"common.views":`Просмотры`,"common.yes":`Да`,"route.accounts":`Аккаунты`,"route.accountsSubtitle":`Консоль / Аккаунты`,"route.channels":`Супергруппы и каналы`,"route.channelsSubtitle":`Консоль / Каналы`,"route.moderation":`Жалобы и модерация`,"route.moderationSubtitle":`Консоль / Модерация`,"route.dashboard":`Панель управления`,"route.dashboardSubtitle":`Консоль / Обзор`,"route.messages":`Аудит сообщений`,"route.messagesSubtitle":`Консоль / Сообщения`,"route.gifts":`Звёздные подарки`,"route.giftsSubtitle":`Консоль / Звёздные подарки`,"route.giveGifts":`Выдача подарков`,"route.giveGiftsSubtitle":`Консоль / Выдача подарков`,"layout.navigation":`Навигация`,"layout.primaryNav":`Основное меню`,"layout.dashboard":`Обзор`,"layout.accounts":`Аккаунты`,"layout.channels":`Супергруппы / Каналы`,"layout.moderation":`Жалобы / Модерация`,"layout.messages":`Сообщения`,"layout.gifts":`Звёздные подарки`,"layout.giveGifts":`Выдача подарков`,"layout.privateMessages":`Личные`,"layout.groupMessages":`Группы`,"layout.runtime":`Среда выполнения`,"layout.adminBackend":`Админ-бэкенд`,"layout.ready":`Готов`,"layout.pgRead":`Чтение из PG`,"layout.readOnly":`Только чтение`,"layout.writeOps":`Операции записи`,"layout.dryRun":`Тестовый запуск`,"layout.actor":`Вы вошли как: {actor}`,"layout.logout":`Выйти`,"language.en":`EN`,"language.zh":`中文`,"language.ru":`RU`,"theme.switchToDark":`Тёмная тема`,"theme.switchToLight":`Светлая тема`,"login.heading":`Панель администратора`,"login.body":`Введите учётные данные для входа в консоль.`,"login.secret":`Пароль или токен администратора`,"login.submit":`Войти`,"login.submitting":`Вход…`,"dashboard.eyebrow":`Состояние системы`,"dashboard.title":`Обзор консоли`,"dashboard.readPath":`Путь чтения`,"dashboard.readPathValue":`PG только для чтения`,"dashboard.writePath":`Путь записи`,"dashboard.executionPolicy":`Политика выполнения`,"dashboard.dryRunFirst":`Сначала тестовый запуск`,"dashboard.accountsText":`Статус аккаунтов, Premium, подтверждение, сессии.`,"dashboard.channelsText":`Публичные каналы и группы, число участников, статус подтверждения.`,"dashboard.messagesText":`Ящики сообщений, обновления, состояние исходящих.`,"dashboard.strip.dryRun":`Все опасные действия начинаются с тестового запуска`,"dashboard.strip.token":`Браузер никогда не сохраняет внутренние токены`,"dashboard.strip.pagination":`Списки используют курсорную пагинацию`,"dashboard.strip.snapshot":`Детальные страницы сохраняют моментальные снимки исходного состояния`,"account.pageTitle":`Аккаунты`,"account.queryResults":`Результаты поиска`,"account.recentActive":`Недавно активные аккаунты`,"account.currentPage":`Аккаунты на странице`,"account.onlineDevices":`Активные сессии устройств`,"account.premium":`Premium`,"account.frozen":`Заморожен`,"account.searchPlaceholder":`ID пользователя / телефон / имя пользователя`,"account.userID":`ID пользователя`,"account.phone":`Телефон`,"account.lastActive":`Последняя активность`,"account.notVerified":`Не подтверждён`,"account.notPremium":`Без Premium`,"account.premiumUntil":`Premium истекает`,"account.starsBalance":`Баланс Звёзд`,"account.startingGrantApplied":`стартовый бонус начислен`,"account.startingGrantPending":`ожидает стартового бонуса`,"account.activeSessions":`Авторизованные устройства`,"account.accountFlags":`Флаги аккаунта`,"account.restriction":`Ограничение`,"account.restricted":`Ограничен`,"account.createdAt":`Создан`,"account.detailTitle":`Аккаунт #{id}`,"account.profile":`Профиль аккаунта`,"account.loadingDetail":`Загрузка данных аккаунта`,"account.waitingData":`Ожидание данных`,"account.noUsername":`Нет имени пользователя`,"account.noPhone":`Нет телефона`,"account.accountFrozen":`Аккаунт заморожен`,"account.accountActive":`Аккаунт активен`,"account.authorizationsTitle":`Авторизованные устройства`,"account.authorizationsCount":`Авторизаций: {count}`,"account.recentAdminOps":`Последние действия администратора`,"account.recent30Audit":`Последние 30 записей аудита`,"account.actionDock":`Действия с аккаунтом`,"account.freezeAccount":`Заморозить аккаунт`,"account.updateFreeze":`Обновить параметры заморозки`,"account.unfreezeAccount":`Разморозить аккаунт`,"account.freezeSince":`Заморожен с`,"account.freezeUntil":`Срок подачи апелляции`,"account.freezeUntilAria":`Срок подачи апелляции на заморозку`,"account.freezeAppealURL":`URL для апелляции`,"account.freezeAppealURLAria":`URL для апелляции на заморозку`,"account.premiumMonths":`Срок действия Premium (в месяцах)`,"account.premiumMonthsAria":`Указать срок действия Premium в месяцах`,"account.setPremium":`Выдать Premium`,"account.clearPremium":`Снять Premium`,"account.starsAmount":`Количество Звёзд`,"account.starsAmountAria":`Указать количество начисляемых Звёзд`,"account.grantStars":`Начислить Звёзды`,"account.setVerified":`Подтвердить аккаунт`,"account.clearVerified":`Снять подтверждение`,"channel.pageTitle":`Супергруппы и каналы`,"channel.recentUpdated":`Недавно обновлённые`,"channel.currentPage":`Объекты на странице`,"channel.megagroups":`Супергруппы`,"channel.broadcasts":`Каналы`,"channel.verifiedCount":`Подтверждённые`,"channel.searchPlaceholder":`ID канала / имя пользователя / название`,"channel.channelID":`ID канала`,"channel.kind":`Тип`,"channel.title":`Название`,"channel.pts":`PTS`,"channel.detailProfile":`Профиль канала`,"channel.loadingDetail":`Загрузка данных канала`,"channel.creator":`Создатель: {id}`,"channel.governance":`Модерация`,"channel.governanceValue":`Заблокировано {banned} / Исключено {kicked}`,"channel.flags":`Флаги канала`,"channel.rawRow":`Исходная строка БД`,"channel.rawRowText":`Снимок базы данных только для чтения`,"channel.actionDock":`Действия с каналом`,"channel.setVerified":`Подтвердить канал`,"channel.clearVerified":`Снять подтверждение`,"channel.kind.broadcast":`Канал`,"channel.kind.forum":`Супергруппа / Форум`,"channel.kind.megagroup":`Супергруппа`,"channel.kind.generic":`Канал / Группа`,"route.bots":`Боты`,"route.botsSubtitle":`Консоль / Боты`,"layout.bots":`Боты`,"bots.pageTitle":`Боты`,"bots.queryResults":`Результаты поиска`,"bots.recent":`Недавно созданные боты`,"bots.currentPage":`Боты на странице`,"bots.banned":`Забанен`,"bots.active":`Активен`,"bots.createTitle":`Создать системного бота`,"bots.createHint":`Создаёт бота, принадлежащего указанному пользователю. Токен показывается один раз после подтверждения.`,"bots.ownerUserID":`ID владельца`,"bots.name":`Отображаемое имя`,"bots.namePlaceholder":`например, Service Bot`,"bots.username":`Имя пользователя`,"bots.usernameHint":`Имя пользователя: 5–32 символа, обязательно оканчивается на «bot».`,"bots.create":`Создать бота`,"bots.searchPlaceholder":`ID бота / имя пользователя`,"bots.botID":`ID бота`,"bots.owner":`Владелец`,"bots.status":`Статус`,"bots.detailTitle":`Бот #{id}`,"bots.profile":`Профиль бота`,"bots.loadingDetail":`Загрузка данных бота`,"bots.unnamed":`Без имени`,"bots.restriction":`Ограничение`,"bots.actionDock":`Действия с ботом`,"bots.banUntil":`Забанить до`,"bots.ban":`Забанить бота`,"bots.updateBan":`Обновить бан`,"bots.unban":`Разбанить бота`,"bots.type":`Тип`,"bots.system":`Системный`,"bots.user":`Пользовательский`,"bots.delete":`Удалить бота`,"bots.deleteHint":`Безвозвратно удаляет созданного пользователем бота и аннулирует его токен. Действие необратимо.`,"bots.systemHint":`Системные боты встроены и не могут быть удалены.`,"flags.scam":`SCAM`,"flags.fake":`FAKE`,"flags.setScam":`Пометить как SCAM`,"flags.clearScam":`Снять метку SCAM`,"flags.setFake":`Пометить как FAKE`,"flags.clearFake":`Снять метку FAKE`,"attr.attributes":`Атрибуты`,"attr.settings":`Настройки`,"attr.username":`Имя пользователя`,"attr.setUsername":`Задать имя пользователя`,"attr.setSupport":`Пометить как support`,"attr.clearSupport":`Снять support`,"attr.forProfile":`Цвет профиля`,"attr.hasColor":`Включить цвет`,"attr.colorIndex":`Индекс цвета`,"attr.bgEmojiID":`ID фонового эмодзи`,"attr.setColor":`Задать цвет`,"attr.emojiDocID":`ID документа эмодзи`,"attr.emojiUntil":`До (unix, 0 = бессрочно)`,"attr.setEmojiStatus":`Задать emoji-статус`,"attr.gigagroup":`Гигагруппа`,"attr.antispam":`Агрессивный антиспам`,"attr.participantsHidden":`Скрыть участников`,"attr.noforwards":`Запретить пересылку`,"attr.joinToSend":`Вступление для отправки`,"attr.joinRequest":`Вступление по заявке`,"attr.slowmode":`Медленный режим (сек)`,"attr.applySettings":`Применить настройки`,"route.emoji":`Emoji`,"route.emojiSubtitle":`Консоль / Emoji`,"layout.emoji":`Emoji`,"emoji.pageTitle":`Кастом-эмодзи`,"emoji.queryResults":`Результаты поиска`,"emoji.recent":`Каталог кастом-эмодзи`,"emoji.currentPage":`Эмодзи на странице`,"emoji.searchPlaceholder":`ID документа или эмодзи`,"emoji.copyID":`Скопировать ID документа`,"emoji.noSet":`Без набора`,"emoji.hint":`ID документов отсюда можно вставлять в поле Emoji-статуса в профилях аккаунтов, ботов и каналов.`,"messages.privateTitle":`Личные сообщения`,"messages.privateEyebrow":`Личные ящики сообщений`,"messages.groupTitle":`Групповые сообщения`,"messages.groupEyebrow":`Сообщения супергрупп и каналов`,"messages.selectPrivatePeers":`Сначала найдите и выберите владельца и собеседника`,"messages.selectChannel":`Сначала найдите и выберите супергруппу или канал`,"messages.ownerUser":`Пользователь-владелец`,"messages.peerUser":`Собеседник`,"messages.beforeDatePlaceholder":`курсор before_date`,"messages.beforeIDPlaceholder":`курсор before_msg_id`,"messages.limitPlaceholder":`лимит <= 100`,"messages.searchMessages":`Поиск сообщений`,"messages.nextPage":`Следующая страница`,"messages.currentPage":`Сообщения на странице`,"messages.deleted":`Удалено`,"messages.outgoing":`Исходящее`,"messages.incoming":`Входящее`,"messages.ownerPeer":`Владелец / Собеседник`,"messages.deleteSelected":`Удалить выбранные сообщения`,"messages.idsPlaceholder":`ID сообщений через запятую`,"messages.revoke":`Удалить для обеих сторон`,"messages.previewDelete":`Тестовое удаление`,"messages.clearHistory":`Очистить историю личной переписки`,"messages.maxIDPlaceholder":`граница max_id`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Очистить только у себя`,"messages.previewClearHistory":`Тестовая очистка истории`,"messages.direction":`Направление`,"messages.body":`Текст сообщения`,"messages.privateDetailTitle":`Сообщение #{id}`,"messages.detailEyebrow":`Детали сообщения`,"messages.backPrivate":`Назад к личным сообщениям`,"messages.backGroup":`Назад к групповым сообщениям`,"messages.ownerPeerTitle":`Владелец {owner} · Собеседник {peer}`,"messages.senderSubtitle":`Отправитель {sender} · {date}`,"messages.boxID":`ID ящика сообщений`,"messages.privateMessageID":`ID личного сообщения`,"messages.messageSender":`Отправитель сообщения`,"messages.messageBox":`Ящик сообщений`,"messages.dialogRow":`Строка диалога`,"messages.privateRow":`Строка личного сообщения`,"messages.channelMessageRow":`Строка сообщения канала`,"messages.channelRow":`Строка канала`,"messages.userUpdateEvents":`События обновления пользователей`,"messages.channelUpdateEvents":`События обновления каналов`,"messages.eventJson":`JSON события`,"messages.dispatchOutbox":`Очередь отправки (Outbox)`,"messages.messageBoxesSnapshot":`Снимок message_boxes только для чтения`,"messages.dialogSnapshot":`Снимок dialogs только для чтения`,"messages.privateSnapshot":`Снимок private_messages только для чтения`,"messages.channelMessagesSnapshot":`Снимок channel_messages только для чтения`,"messages.channelSnapshot":`Снимок channels только для чтения`,"messages.userEventsSource":`постоянные user_update_events`,"messages.channelEventsSource":`постоянные channel_update_events`,"messages.outboxSource":`онлайн/офлайн dispatch_outbox`,"messages.attempts":`Попытки`,"messages.deleteThis":`Удалить это сообщение`,"messages.groupDetailTitle":`Групповое сообщение #{id}`,"messages.channelGroupTitle":`Канал / Группа {id}`,"messages.mediaCount":`С медиафайлами`,"messages.channelPosts":`Посты канала`,"messages.channelGroup":`Канал / Группа`,"messages.pinned":`Закреплено`,"messages.channelPost":`Пост в канале`,"gifts.pageTitle":`Каталог звёздных подарков`,"giveGift.action":`Выдать`,"giveGift.eyebrow":`Выдача подарка · без списания`,"giveGift.title":`Выдать подарок`,"giveGift.recipientKind":`Тип получателя`,"giveGift.recipientUser":`Пользователь`,"giveGift.recipientChannel":`Канал`,"giveGift.pickUser":`Получатель (пользователь)`,"giveGift.pickChannel":`Получатель (канал)`,"giveGift.recipientRequired":`Сначала выберите получателя`,"giveGift.sender":`ID аккаунта-отправителя`,"giveGift.senderHint":`Подарки всегда отправляются от системного аккаунта 777000 (Telesrv).`,"giveGift.message":`Сообщение к подарку (необязательно)`,"giveGift.messagePlaceholder":`Показывается вместе с подарком`,"giveGift.hideName":`Скрыть имя отправителя от получателя`,"giveGift.upgrade":`Выдать как улучшенный коллекционный`,"giveGift.upgradeNote":`Подарок будет отчеканен как уникальный коллекционный. Ниже можно выбрать конкретные атрибуты или оставить «Случайно» для выбора из опубликованного пула. Номер присваивается автоматически. Требуется опубликованное коллекционное улучшение с остатком тиража.`,"giveGift.model":`Модель`,"giveGift.pattern":`Узор`,"giveGift.backdrop":`Фон`,"giveGift.random":`Случайно`,"giveGift.confirm":`Выдать подарок`,"giveGifts.pageTitle":`Выдача подарков`,"giveGifts.eyebrow":`Выдача каталожных подарков любому пользователю или каналу`,"giveGifts.available":`Доступно подарков`,"giveGifts.sender":`Отправитель по умолчанию`,"giveGifts.searchPlaceholder":`Поиск по названию или ID подарка`,"giveGifts.hint":`Выберите подарок для выдачи. Выдача бесплатна и по умолчанию отправляется от системного аккаунта 777000 (Telesrv).`,"giveGifts.pickGift":`Выберите подарок`,"giveGifts.selectPrompt":`Выберите подарок из списка, чтобы начать.`,"gifts.eyebrow":`Каталог, неизменяемые версии и файлы анимаций`,"gifts.total":`Подарков в каталоге`,"gifts.enabled":`Включено`,"gifts.received":`Полученные подарки`,"gifts.formats":`Поддерживаемые форматы`,"gifts.add":`Добавить подарок`,"gifts.searchPlaceholder":`Поиск по ID подарка, названию или формату`,"gifts.listSummary":`Показано {shown} из {total}`,"gifts.idRevision":`ID / Версия`,"gifts.price":`Цена / Конвертация`,"gifts.importTitle":`Импорт звёздного подарка`,"gifts.importEyebrow":`Управление каталогом подарков`,"gifts.newRevision":`Создать версию для подарка #{id}`,"gifts.importHint":`Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.`,"gifts.officialSource":`Официальный снимок`,"gifts.fileSource":`Загрузить файл`,"gifts.officialHint":`Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.`,"gifts.officialSearch":`Поиск по ID или названию официального подарка`,"gifts.officialSelect":`Выберите официальный подарок`,"gifts.officialRequired":`Сначала выберите официальный подарок`,"gifts.officialResults":`Показано {shown} из {total}`,"gifts.officialCategoryLabel":`Категория возможностей официального подарка`,"gifts.officialCategory.all":`Все`,"gifts.officialCategory.upgrade":`Можно улучшить`,"gifts.officialCategory.craft":`Можно создать`,"gifts.officialCategory.basic":`Нельзя улучшить`,"gifts.officialUnnamed":`Официальный подарок без названия #{id}`,"gifts.officialAttributes":`Атрибутов: {count}`,"gifts.canUpgrade":`Можно улучшить`,"gifts.cannotUpgrade":`Нельзя улучшить`,"gifts.canCraft":`Можно создать`,"gifts.cannotCraft":`Нельзя создать`,"gifts.officialEmpty":`Нет подарков, соответствующих категории и поиску.`,"gifts.includeCollectible":`Импортировать полный пул коллекционных предметов, включая созданные модели`,"gifts.animation":`Файл анимации`,"gifts.filePrompt":`Перетащите или выберите файл TGS / Lottie`,"gifts.fileHint":`TGS, JSON или Lottie · файл проверяется перед импортом`,"gifts.chooseFile":`Выбрать файл`,"gifts.changeFile":`Изменить файл`,"gifts.title":`Отображаемое название`,"gifts.titlePlaceholder":`например, Праздничная звезда`,"gifts.stars":`Цена в Звёздах`,"gifts.convertStars":`Звёзд при конвертации`,"gifts.sortOrder":`Порядок сортировки`,"gifts.reason":`Причина для аудита`,"gifts.reasonPlaceholder":`Кратко опишите причину импорта этого подарка`,"gifts.enableAfterImport":`Включить после импорта`,"gifts.validate":`Тестовая проверка`,"gifts.confirmImport":`Подтвердить импорт`,"gifts.stepDetails":`Файл и описание`,"gifts.stepValidate":`Тестовая проверка`,"gifts.stepImport":`Подтверждение импорта`,"gifts.fileRequired":`Сначала выберите файл TGS или Lottie`,"gifts.source":`Источник`,"gifts.replace":`Новая версия`,"gifts.disable":`Отключить`,"gifts.enable":`Включить`,"gifts.empty":`Звёздные подарки ещё не импортированы.`,"gifts.emptyHint":`Импортируйте первую анимацию, чтобы начать наполнение каталога.`,"gifts.validationReady":`Проверка пройдена`,"gifts.validationHint":`Проверьте нормализованные метаданные и подтвердите импорт.`,"gifts.confirmState":`Применить проверенные изменения состояния к подарку #{id}?`,"collectibles.manage":`Пул атрибутов`,"collectibles.title":`Пул коллекционных предметов · Подарок #{id}`,"collectibles.eyebrow":`Уникальные атрибуты подарка`,"collectibles.activeRevision":`Опубликованная версия {revision}`,"collectibles.published":`Опубликовано`,"collectibles.noPool":`Нет опубликованного пула коллекционных предметов`,"collectibles.noPoolHint":`Опубликуйте модели, узоры и фоны для активации улучшений.`,"collectibles.publishNew":`Опубликовать новую неизменяемую версию`,"collectibles.immutableHint":`Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.`,"collectibles.upgradeStars":`Цена улучшения в Звёздах`,"collectibles.supply":`Уникальный тираж`,"collectibles.slug":`Публичный префикс ссылки (slug)`,"collectibles.models":`Модели`,"collectibles.patterns":`Узоры`,"collectibles.backdrops":`Фоны`,"collectibles.model":`Модель`,"collectibles.pattern":`Узор`,"collectibles.backdrop":`Фон`,"collectibles.rarity":`Редкость ‰`,"collectibles.rarityHint":`В каждой категории должно быть не менее двух атрибутов. Значения в промилле задают относительные веса обычного улучшения; при добавлении или удалении они перераспределяются до суммы 1000.`,"collectibles.minimumAttributes":`Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.`,"collectibles.duplicateBackdropID":`ID фонов в одном наборе должны быть уникальными.`,"collectibles.colorHint":`Цвета сохраняются как 24-битные RGB-значения.`,"collectibles.addAttribute":`Добавить`,"collectibles.remove":`Удалить атрибут`,"collectibles.fileRequired":`Для каждой модели и узора требуется файл TGS или Lottie.`,"collectibles.backdropID":`ID фона`,"collectibles.color.center":`Центр`,"collectibles.color.edge":`Край`,"collectibles.color.pattern":`Узор`,"collectibles.color.text":`Текст`,"collectibles.validationReady":`Пул атрибутов корректен`,"collectibles.validationHint":`Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.`,"collectibles.publish":`Опубликовать версию`,"moderation.casesEyebrow":`Модерация / Обращения`,"moderation.currentQueue":`Текущая очередь`,"moderation.criticalCases":`Критические обращения`,"moderation.pendingOrFailed":`Ожидающие / неудачные действия`,"moderation.statusFilter":`Фильтр по статусу обращения`,"moderation.statusFilter.active":`Активная очередь`,"moderation.statusFilter.all":`Все статусы`,"moderation.assignee":`Модератор`,"moderation.allAssignees":`Оставьте пустым для всех`,"moderation.case":`Обращение`,"moderation.target":`Объект`,"moderation.severity":`Важность`,"moderation.reportsAndReporters":`Жалобы / Авторы`,"moderation.latestReport":`Последняя жалоба`,"moderation.review":`Проверить`,"moderation.status.open":`Открыто`,"moderation.status.in_review":`На рассмотрении`,"moderation.status.action_pending":`Ожидает действия`,"moderation.status.action_failed":`Ошибка действия`,"moderation.status.resolved":`Решено`,"moderation.status.dismissed":`Отклонено`,"moderation.status.appeal_review":`Рассмотрение апелляции`,"moderation.severity.low":`Низкая`,"moderation.severity.medium":`Средняя`,"moderation.severity.high":`Высокая`,"moderation.severity.critical":`Критическая`,"moderation.targetType.user":`Аккаунт`,"moderation.targetType.chat":`Группа`,"moderation.targetType.channel":`Канал`,"moderation.caseDetailTitle":`Проверка обращения #{id}`,"moderation.caseDetailEyebrow":`Модерация / Детали обращения`,"moderation.backToQueue":`Назад к очереди`,"moderation.loadingCase":`Загрузка обращения…`,"moderation.versionAndUpdated":`Версия {version} · Обновлено {time}`,"moderation.reportCount":`Жалобы`,"moderation.reportCountValue":`Жалоб: {reports}; авторов: {reporters}`,"moderation.firstAndLatestReport":`Первая / последняя жалоба`,"moderation.evidence":`Материалы жалобы`,"moderation.evidenceHint":`Показываются последние 100 жалоб; снимки фиксируются при приёме жалобы.`,"moderation.sourceAndReason":`Источник / Причина`,"moderation.reporter":`Автор жалобы`,"moderation.option":`Вариант`,"moderation.decisionAudit":`Аудит решений и действий`,"moderation.decisionAuditHint":`Действия выполняются идемпотентно арендующим worker-процессом; при сбое сохраняются ошибка и число попыток.`,"moderation.appeals":`Апелляции`,"moderation.caseActions":`Действия с обращением`,"moderation.renewClaim":`Продлить назначение`,"moderation.claimCase":`Взять на проверку`,"moderation.reviewReason":`Причина решения`,"moderation.decisionPreset":`Шаблон решения`,"moderation.preset.noViolation":`Нет нарушения (отклонить жалобу)`,"moderation.preset.scam":`Пометить как SCAM`,"moderation.preset.fake":`Пометить как FAKE`,"moderation.preset.freeze":`Заморозить аккаунт`,"moderation.preset.scamFreeze":`SCAM + заморозка`,"moderation.preset.fakeFreeze":`FAKE + заморозка`,"moderation.preset.deleteMessages":`Удалить сообщения из материалов`,"moderation.preset.deleteAccount":`Удалить аккаунт`,"moderation.evidenceMessageIDs":`ID сообщений из материалов (через запятую)`,"moderation.privateOwnerUserID":`owner_user_id личного чата`,"moderation.revokeForBoth":`Удалить у обеих сторон`,"moderation.evidenceValidationHint":`Сервер повторно проверит, что каждый ID сообщения присутствует в неизменяемых материалах этого обращения.`,"moderation.failedActionHint":`Действие выполнено частично, поэтому решение нельзя сразу сменить на отсутствие нарушения. Выберите новое действие для повтора; аудит предыдущего сбоя сохранится.`,"moderation.retryAction":`Повторить действие`,"moderation.submitDecision":`Отправить решение`,"moderation.appealReviewTitle":`Рассмотрение апелляции #{id}`,"moderation.automaticRemedy":`Автовосстановление после одобрения`,"moderation.irreversibleAppealHint":`Обращение содержит завершённое необратимое удаление. Его нельзя отметить как одобренное и восстановленное; отклоните или передайте на ручную обработку.`,"moderation.denyAppeal":`Отклонить апелляцию`,"moderation.grantAppeal":`Одобрить апелляцию`,"moderation.reasonRequired":`Укажите причину решения.`,"moderation.appealReasonRequired":`Укажите причину рассмотрения апелляции.`,"moderation.privateDeleteValidation":`Для удаления личных сообщений нужны корректные ID сообщений из материалов и owner_user_id автора жалобы.`,"moderation.channelDeleteValidation":`Для удаления сообщений канала нужен хотя бы один корректный ID сообщения из материалов.`,"moderation.confirmDecision":`Отправить решение «{decision}»? Действие будет выполнено через устойчивую очередь.`,"moderation.confirmGrantAppeal":`Одобрить эту апелляцию?`,"moderation.confirmDenyAppeal":`Отклонить эту апелляцию?`,"moderation.remedy.clearFlags":`Снять SCAM / FAKE`,"moderation.remedy.unfreeze":`Разморозить аккаунт`,"moderation.remedy.none":`Восстановление не требуется`,"moderation.source.account_peer":`Аккаунт / пир`,"moderation.source.profile_photo":`Фото профиля`,"moderation.source.messages_spam":`Спам в сообщениях`,"moderation.source.messages":`Сообщения`,"moderation.source.encrypted_spam":`Спам в секретном чате`,"moderation.source.reaction":`Реакция`,"moderation.source.channel_spam":`Спам в канале`,"moderation.source.story":`История`,"moderation.source.ephemeral":`Исчезающее медиа`,"moderation.source.sponsored":`Рекламное сообщение`,"moderation.source.antispam_false_positive":`Ложное срабатывание антиспама`,"moderation.reason.spam":`Спам`,"moderation.reason.violence":`Насилие`,"moderation.reason.pornography":`Порнография`,"moderation.reason.child_abuse":`Жестокое обращение с детьми`,"moderation.reason.other":`Другое`,"moderation.reason.copyright":`Авторские права`,"moderation.reason.geo_irrelevant":`Не относится к региону`,"moderation.reason.fake":`Подделка`,"moderation.reason.illegal_drugs":`Незаконные наркотики`,"moderation.reason.personal_details":`Персональные данные`,"messages.msgIDsInvalid":`Некорректные ID сообщений`,"auth.device":`Устройство`,"auth.platform":`Платформа`,"auth.ip":`IP-адрес`,"auth.lastActive":`Последняя активность`,"auth.revokeCurrent":`Отозвать текущую`,"auth.keepCurrent":`Оставить текущую`,"auth.revokeAll":`Отозвать все устройства`,"picker.userPlaceholder":`Поиск по user_id / телефону / имени пользователя`,"picker.channelPlaceholder":`Поиск по channel_id / имени пользователя / названию`,"picker.verified":`Подтверждённые`,"picker.regular":`Обычные`,"action.reasonRequired":`Пожалуйста, укажите причину операции`,"action.flow":`Процесс выполнения`,"action.close":`Закрыть`,"action.stepReason":`Укажите причину`,"action.stepDryRun":`Тестовый запуск`,"action.stepConfirm":`Подтверждение выполнения`,"action.reason":`Причина операции`,"action.reasonPlaceholder":`Опишите, почему выполняется эта операция`,"action.requestPreview":`Запросить предпросмотр`,"action.result":`Результат действия`,"action.commandID":`ID команды`,"action.status":`Статус`,"action.dryRun":`Тестовый запуск`,"action.runAgain":`Повторить тестовый запуск`,"action.runDry":`Сначала выполните тестовый запуск`,"action.confirm":`Подтвердить выполнение`,"audit.id":`ID`,"audit.commandID":`ID команды`,"audit.action":`Действие`,"audit.actor":`Исполнитель`,"audit.status":`Статус`,"audit.dryRun":`Тестовый запуск`,"audit.reason":`Причина`,"audit.time":`Время`}},Fe=(0,g.createContext)(null);function Ie({children:e}){let[t,n]=(0,g.useState)(()=>ze());(0,g.useEffect)(()=>{try{localStorage.setItem(Ne,t)}catch{}let e=t===`zh`?`zh-CN`:t===`ru`?`ru`:`en`;document.documentElement.lang=e,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=Re(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>Re(t,e,n)}),[t]);return(0,H.jsx)(Fe.Provider,{value:r,children:e})}function U(){let e=(0,g.useContext)(Fe);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function Le(){let{lang:e,setLang:t,t:n}=U();return(0,H.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`,`ru`].map(r=>(0,H.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function Re(e,t,n){let r=Pe[e][t]??Pe.en[t]??t;return n?r.replace(/\{(\w+)\}/g,(e,t)=>String(n[t]??``)):r}function ze(){try{let e=Be(new URLSearchParams(window.location.search).get(`lang`));if(e)return e}catch{}try{let e=Be(localStorage.getItem(Ne));if(e)return e}catch{}let e=navigator.languages?.length?navigator.languages:[navigator.language];for(let t of e){let e=Be(t);if(e)return e}return`en`}function Be(e){if(!e)return null;let t=e.trim().toLowerCase().replace(`_`,`-`);return t===`zh`||t.startsWith(`zh-`)?`zh`:t===`en`||t.startsWith(`en-`)?`en`:t===`ru`||t.startsWith(`ru-`)?`ru`:null}function W(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function Ve(e,t){return e.startsWith(`/accounts`)?t(`route.accounts`):e.startsWith(`/channels`)?t(`route.channels`):e.startsWith(`/bots`)?t(`route.bots`):e.startsWith(`/moderation`)?t(`route.moderation`):e.startsWith(`/emoji`)?t(`route.emoji`):e.startsWith(`/messages`)?t(`route.messages`):e.startsWith(`/give-gifts`)?t(`route.giveGifts`):e.startsWith(`/gifts`)?t(`route.gifts`):t(`route.dashboard`)}function He(e,t){return e.startsWith(`/accounts`)?t(`route.accountsSubtitle`):e.startsWith(`/channels`)?t(`route.channelsSubtitle`):e.startsWith(`/bots`)?t(`route.botsSubtitle`):e.startsWith(`/moderation`)?t(`route.moderationSubtitle`):e.startsWith(`/emoji`)?t(`route.emojiSubtitle`):e.startsWith(`/messages`)?t(`route.messagesSubtitle`):e.startsWith(`/give-gifts`)?t(`route.giveGiftsSubtitle`):e.startsWith(`/gifts`)?t(`route.giftsSubtitle`):t(`route.dashboardSubtitle`)}var Ue=`telesrv.admin.theme`,We=(0,g.createContext)(null);function Ge(e){document.documentElement.setAttribute(`data-theme`,e),document.documentElement.style.colorScheme=e}function Ke({children:e}){let[t,n]=(0,g.useState)(()=>Ye());(0,g.useEffect)(()=>{Ge(t);try{localStorage.setItem(Ue,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(Ue)}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,H.jsx)(We.Provider,{value:a,children:e})}function qe(){let e=(0,g.useContext)(We);if(!e)throw Error(`useTheme must be used inside ThemeProvider`);return e}function Je(){let{theme:e,toggleTheme:t}=qe(),{t:n}=U(),r=n(e===`light`?`theme.switchToDark`:`theme.switchToLight`);return(0,H.jsx)(`button`,{className:`theme-toggle`,type:`button`,onClick:t,"aria-label":r,title:r,children:e===`dark`?(0,H.jsx)(Ee,{size:16}):(0,H.jsx)(fe,{size:16})})}function Ye(){try{let e=localStorage.getItem(Ue);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 G({href:e,navigate:t,className:n,children:r}){return(0,H.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function Xe(){let{t:e}=U();return(0,H.jsxs)(`div`,{className:`boot-screen`,children:[(0,H.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,H.jsx)(`div`,{className:`loader-bar`})]})}function Ze({actor:e,route:t,navigate:n,onLogout:r,children:i}){let{t:a}=U(),o=t.path.startsWith(`/messages`),[s,c]=(0,g.useState)(o);(0,g.useEffect)(()=>{o&&c(!0)},[o]);async function l(){await x.logout().catch(()=>void 0),r()}return(0,H.jsxs)(`div`,{className:`shell`,children:[(0,H.jsxs)(`aside`,{className:`sidebar`,children:[(0,H.jsxs)(G,{className:`brand`,href:`/`,navigate:n,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,H.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,H.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,H.jsx)(Qe,{icon:(0,H.jsx)(ue,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,H.jsx)(Qe,{icon:(0,H.jsx)(Ae,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,H.jsx)(Qe,{icon:(0,H.jsx)(Se,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,H.jsx)(Qe,{icon:(0,H.jsx)(F,{size:16}),href:`/bots`,route:t,navigate:n,children:a(`layout.bots`)}),(0,H.jsx)(Qe,{icon:(0,H.jsx)(xe,{size:16}),href:`/moderation`,route:t,navigate:n,children:a(`layout.moderation`)}),(0,H.jsx)(Qe,{icon:(0,H.jsx)(se,{size:16}),href:`/gifts`,route:t,navigate:n,children:a(`layout.gifts`)}),(0,H.jsx)(Qe,{icon:(0,H.jsx)(ye,{size:16}),href:`/give-gifts`,route:t,navigate:n,children:a(`layout.giveGifts`)}),(0,H.jsx)(Qe,{icon:(0,H.jsx)(we,{size:16}),href:`/emoji`,route:t,navigate:n,children:a(`layout.emoji`)}),(0,H.jsxs)(`div`,{className:`nav-section ${o?`active`:``} ${s?`open`:``}`,children:[(0,H.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":s,onClick:()=>c(e=>!e),children:[(0,H.jsx)(B,{size:16}),(0,H.jsx)(`span`,{children:a(`layout.messages`)}),(0,H.jsx)(ee,{className:`nav-section-chevron`,size:15})]}),s&&(0,H.jsxs)(`div`,{className:`nav-children`,children:[(0,H.jsx)(Qe,{href:`/messages/private`,route:t,navigate:n,activeWhen:e=>e===`/messages`||e===`/messages/detail`||e.startsWith(`/messages/private`),children:a(`layout.privateMessages`)}),(0,H.jsx)(Qe,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,H.jsxs)(`div`,{className:`sidebar-status`,children:[(0,H.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(be,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,H.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(re,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,H.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(Ce,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,H.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,H.jsxs)(`div`,{className:`workspace`,children:[(0,H.jsxs)(`header`,{className:`topbar`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:He(t.path,a)}),(0,H.jsx)(`h1`,{children:Ve(t.path,a)})]}),(0,H.jsxs)(`div`,{className:`topbar-actions`,children:[(0,H.jsx)(Je,{}),(0,H.jsx)(Le,{}),(0,H.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,H.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:l,title:a(`layout.logout`),children:[(0,H.jsx)(de,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,H.jsx)(`main`,{className:`content`,children:i})]})]})}function Qe({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,H.jsxs)(G,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,H.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,H.jsx)(`span`,{children:i})]})}function $e(e){let t=e.trim();return!t||t.startsWith(`+`)?t:/^\d+$/.test(t)?`+${t}`:t}function et(e){let t=e.trim();return t?t.startsWith(`@`)?t:`@${t}`:``}function tt(e){return`${e.FirstName||``} ${e.LastName||``}`.trim()||`-`}function nt(e,t){let n=t??(e=>({"channel.kind.broadcast":`Channel`,"channel.kind.forum":`Supergroup / Forum`,"channel.kind.megagroup":`Supergroup`,"channel.kind.generic":`Channel / Group`})[e]??e);return e.Broadcast&&!e.Megagroup?n(`channel.kind.broadcast`):e.Megagroup&&e.Forum?n(`channel.kind.forum`):e.Megagroup?n(`channel.kind.megagroup`):n(`channel.kind.generic`)}function rt(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function it(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function at(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function ot(e,t=`msg ids invalid`){let n=e.split(/[\s,]+/).map(e=>e.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}function st({title:e,eyebrow:t,children:n,actions:r}){return(0,H.jsxs)(`div`,{className:`page-frame`,children:[(0,H.jsxs)(`div`,{className:`page-title-row`,children:[(0,H.jsxs)(`div`,{children:[t&&(0,H.jsx)(`div`,{className:`eyebrow`,children:t}),(0,H.jsx)(`h2`,{children:e})]}),r&&(0,H.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function ct({children:e}){return(0,H.jsx)(`div`,{className:`query-panel`,children:e})}function lt({main:e,side:t}){return(0,H.jsxs)(`div`,{className:`split-layout`,children:[(0,H.jsx)(`div`,{className:`split-main`,children:e}),(0,H.jsx)(`aside`,{className:`split-side`,children:t})]})}function K({title:e,text:t,action:n}){return(0,H.jsxs)(`div`,{className:`section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`h2`,{children:e}),t&&(0,H.jsx)(`p`,{children:t})]}),n&&(0,H.jsx)(`div`,{className:`section-action`,children:n})]})}function ut({children:e}){return(0,H.jsxs)(`div`,{className:`alert`,children:[(0,H.jsx)(O,{size:16}),` `,(0,H.jsx)(`span`,{children:e})]})}function q({children:e,tone:t=`neutral`}){return(0,H.jsx)(`span`,{className:`badge ${t}`,children:e})}function dt({label:e,value:t,tone:n}){return(0,H.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{children:t})]})}function J({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,H.jsxs)(`div`,{className:`metric ${n}`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function Y({label:e,value:t,mono:n=!1}){return(0,H.jsxs)(`div`,{className:`summary-item`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function ft({rows:e}){let{t}=U();return(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`audit.id`)}),(0,H.jsx)(`th`,{children:t(`audit.commandID`)}),(0,H.jsx)(`th`,{children:t(`audit.action`)}),(0,H.jsx)(`th`,{children:t(`audit.actor`)}),(0,H.jsx)(`th`,{children:t(`audit.status`)}),(0,H.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,H.jsx)(`th`,{children:t(`audit.reason`)}),(0,H.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[e.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.ID}),(0,H.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,H.jsx)(`td`,{children:e.Action}),(0,H.jsx)(`td`,{children:e.Actor}),(0,H.jsx)(`td`,{children:e.Status}),(0,H.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,H.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,H.jsx)(`td`,{children:rt(e.CreatedAt)})]},e.ID)),e.length===0&&(0,H.jsx)(pt,{colSpan:8})]})]})})}function pt({colSpan:e}){let{t}=U();return(0,H.jsx)(`tr`,{children:(0,H.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function mt({label:e}){return(0,H.jsx)(`section`,{className:`surface`,children:(0,H.jsx)(`div`,{className:`loading-line`,children:e})})}function ht({value:e}){return(0,H.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function gt({onLogin:e}){let{t}=U(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1);async function c(t){t.preventDefault(),s(!0),a(``);try{e((await x.login(n)).actor)}catch(e){a(b(e))}finally{s(!1)}}return(0,H.jsx)(`main`,{className:`login-page`,children:(0,H.jsxs)(`section`,{className:`login-panel`,children:[(0,H.jsxs)(`div`,{className:`login-head`,children:[(0,H.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,H.jsxs)(`div`,{className:`login-head-actions`,children:[(0,H.jsx)(Je,{}),(0,H.jsx)(Le,{}),(0,H.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,H.jsxs)(`div`,{className:`login-copy`,children:[(0,H.jsx)(`h1`,{children:t(`login.heading`)}),(0,H.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,H.jsx)(ut,{children:i}),(0,H.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:t(`login.secret`)}),(0,H.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,H.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var _t=m();function vt({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,onDone:o}){let{t:s}=U(),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(null),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(!1);function y(){d(``),p(null),h(``)}async function S(e){if(!u.trim()){h(s(`action.reasonRequired`));return}v(!0),h(``);try{let r={...n(),reason:u,confirm:e};p(await x.action(t,r)),e&&o?.()}catch(e){h(b(e))}finally{v(!1)}}let C=f?.dry_run&&!f.error,w=`btn ${a===`danger`?`danger`:a===`warn`?`warn`:``} ${i?`compact-btn`:``}`,T=(0,g.useMemo)(()=>{try{return n()}catch(e){return{payload_error:b(e)}}},[c,n]);return(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:w,type:`button`,onClick:()=>{y(),l(!0)},children:[r,e]}),c&&(0,_t.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:s(`action.flow`)}),(0,H.jsx)(`h2`,{children:e})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>l(!1),"aria-label":s(`action.close`),children:(0,H.jsx)(je,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body`,children:[(0,H.jsxs)(`div`,{className:`command-steps`,children:[(0,H.jsxs)(`div`,{className:`command-step ${u.trim()?`done`:`active`}`,children:[(0,H.jsx)(`span`,{children:`1`}),(0,H.jsx)(`strong`,{children:s(`action.stepReason`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${f?.dry_run?`done`:u.trim()?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`2`}),(0,H.jsx)(`strong`,{children:s(`action.stepDryRun`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${f&&!f.dry_run&&!f.error?`done`:C?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`3`}),(0,H.jsx)(`strong`,{children:s(`action.stepConfirm`)})]})]}),(0,H.jsxs)(`label`,{className:`form-field`,children:[(0,H.jsx)(`span`,{children:s(`action.reason`)}),(0,H.jsx)(`textarea`,{value:u,onChange:e=>d(e.target.value),rows:3,placeholder:s(`action.reasonPlaceholder`)})]}),(0,H.jsxs)(`div`,{className:`command-preview`,children:[(0,H.jsxs)(`div`,{className:`preview-head`,children:[(0,H.jsx)(ae,{size:14}),` `,s(`action.requestPreview`)]}),(0,H.jsx)(ht,{value:JSON.stringify(T,null,2)})]}),m&&(0,H.jsx)(ut,{children:m}),f&&(0,H.jsxs)(`div`,{className:`result-box`,children:[(0,H.jsxs)(`div`,{className:`result-title`,children:[f.error?(0,H.jsx)(O,{size:16}):(0,H.jsx)(k,{size:16}),(0,H.jsx)(`strong`,{children:f.message||f.error||s(`action.result`)})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.commandID`)}),(0,H.jsx)(`strong`,{children:f.command_id})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.status`)}),(0,H.jsx)(`strong`,{children:f.status})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:s(`action.dryRun`)}),(0,H.jsx)(`strong`,{children:f.dry_run?s(`common.yes`):s(`common.no`)})]}),(0,H.jsx)(`div`,{className:`result-message`,children:f.message||f.error}),f.details&&(0,H.jsx)(ht,{value:JSON.stringify(f.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>l(!1),children:s(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:_,children:[_?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(he,{size:15}),s(f?`action.runAgain`:`action.runDry`)]}),(0,H.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>S(!0),disabled:_||!C,children:[(0,H.jsx)(k,{size:15}),s(`action.confirm`)]})]})]})}),document.body)]})}function yt({rows:e,userID:t,onDone:n}){let{t:r}=U(),[i,a]=(0,g.useState)(()=>new Set);(0,g.useEffect)(()=>{a(new Set)},[t]);let o=(0,g.useMemo)(()=>e.filter(e=>!i.has(e.Hash)),[e,i]);function s(e){a(t=>e(t)),n()}return(0,H.jsxs)(`div`,{className:`authorization-block`,children:[(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:r(`auth.device`)}),(0,H.jsx)(`th`,{children:r(`auth.platform`)}),(0,H.jsx)(`th`,{children:r(`auth.ip`)}),(0,H.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,H.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,H.jsxs)(`tbody`,{children:[o.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,H.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,H.jsx)(`td`,{children:n.IP}),(0,H.jsx)(`td`,{children:rt(n.ActiveAt)}),(0,H.jsx)(`td`,{className:`device-actions-cell`,children:(0,H.jsxs)(`div`,{className:`device-actions`,children:[(0,H.jsx)(vt,{label:r(`auth.revokeCurrent`),icon:(0,H.jsx)(de,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,hash:n.Hash}),onDone:()=>s(e=>new Set([...e,n.Hash]))}),(0,H.jsx)(vt,{label:r(`auth.keepCurrent`),icon:(0,H.jsx)(Se,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,keep_hash:n.Hash}),onDone:()=>s(()=>new Set(e.filter(e=>e.Hash!==n.Hash).map(e=>e.Hash)))})]})})]},n.Hash)),o.length===0&&(0,H.jsx)(pt,{colSpan:5})]})]})}),(0,H.jsx)(`div`,{className:`danger-zone`,children:(0,H.jsx)(vt,{label:r(`auth.revokeAll`),icon:(0,H.jsx)(I,{size:15}),path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,revoke_all:!0}),onDone:()=>s(()=>new Set(e.map(e=>e.Hash)))})})]})}function bt({scam:e,fake:t}){let{t:n}=U();return!e&&!t?null:(0,H.jsxs)(H.Fragment,{children:[e&&(0,H.jsx)(q,{tone:`danger`,children:n(`flags.scam`)}),t&&(0,H.jsx)(q,{tone:`danger`,children:n(`flags.fake`)})]})}function xt({idKey:e,id:t,path:n,scam:r,fake:i,onDone:a}){let{t:o}=U();return(0,H.jsxs)(`div`,{className:`action-stack`,children:[(0,H.jsx)(vt,{label:o(r?`flags.clearScam`:`flags.setScam`),icon:(0,H.jsx)(xe,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,scam:!r,fake:r?i:!1}),onDone:a}),(0,H.jsx)(vt,{label:o(i?`flags.clearFake`:`flags.setFake`),icon:(0,H.jsx)(j,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,fake:!i,scam:i?r:!1}),onDone:a})]})}function St({id:e,support:t,onDone:n}){let{t:r}=U();return(0,H.jsx)(vt,{label:r(t?`attr.clearSupport`:`attr.setSupport`),icon:(0,H.jsx)(z,{size:15}),tone:`neutral`,path:`/api/actions/set-support`,payload:()=>({user_id:e,support:!t}),onDone:n})}function Ct({idKey:e,id:t,path:n,current:r,onDone:i}){let{t:a}=U(),[o,s]=(0,g.useState)(r.replace(/^@/,``));return(0,H.jsxs)(`div`,{className:`attr-block`,children:[(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:a(`attr.username`)}),(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:`username`})]}),(0,H.jsx)(vt,{label:a(`attr.setUsername`),icon:(0,H.jsx)(P,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,username:o.trim().replace(/^@/,``)}),onDone:i})]})}function wt({idKey:e,id:t,path:n,onDone:r}){let{t:i}=U(),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(!0),[l,u]=(0,g.useState)(`0`),[d,f]=(0,g.useState)(``);return(0,H.jsxs)(`div`,{className:`attr-block`,children:[(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:a,onChange:e=>o(e.target.checked)}),` `,i(`attr.forProfile`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:s,onChange:e=>c(e.target.checked)}),` `,i(`attr.hasColor`)]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:i(`attr.colorIndex`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,max:`20`,value:l,onChange:e=>u(e.target.value)})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:i(`attr.bgEmojiID`)}),(0,H.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),placeholder:`0`})]}),(0,H.jsx)(vt,{label:i(`attr.setColor`),icon:(0,H.jsx)(pe,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,for_profile:a,has_color:s,color:at(l),background_emoji_id:d.trim()||`0`}),onDone:r})]})}function Tt({idKey:e,id:t,path:n,onDone:r}){let{t:i}=U(),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(`0`);return(0,H.jsxs)(`div`,{className:`attr-block`,children:[(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:i(`attr.emojiDocID`)}),(0,H.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`0 = clear`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:i(`attr.emojiUntil`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:s,onChange:e=>c(e.target.value)})]}),(0,H.jsx)(vt,{label:i(`attr.setEmojiStatus`),icon:(0,H.jsx)(we,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,document_id:a.trim()||`0`,until:at(s)}),onDone:r})]})}function Et({channel:e,onDone:t}){let{t:n}=U(),[r,i]=(0,g.useState)(e.Gigagroup),[a,o]=(0,g.useState)(e.AntiSpam),[s,c]=(0,g.useState)(e.ParticipantsHidden),[l,u]=(0,g.useState)(e.NoForwards),[d,f]=(0,g.useState)(e.JoinToSend),[p,m]=(0,g.useState)(e.JoinRequest),[h,_]=(0,g.useState)(String(e.SlowmodeSeconds));(0,g.useEffect)(()=>{i(e.Gigagroup),o(e.AntiSpam),c(e.ParticipantsHidden),u(e.NoForwards),f(e.JoinToSend),m(e.JoinRequest),_(String(e.SlowmodeSeconds))},[e]);function v(){let t={channel_id:e.ID};return r!==e.Gigagroup&&(t.gigagroup=r),a!==e.AntiSpam&&(t.antispam=a),s!==e.ParticipantsHidden&&(t.participants_hidden=s),l!==e.NoForwards&&(t.noforwards=l),d!==e.JoinToSend&&(t.join_to_send=d),p!==e.JoinRequest&&(t.join_request=p),at(h)!==e.SlowmodeSeconds&&(t.slowmode_seconds=at(h)),t}return(0,H.jsxs)(`div`,{className:`attr-block`,children:[(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:r,onChange:e=>i(e.target.checked)}),` `,n(`attr.gigagroup`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:a,onChange:e=>o(e.target.checked)}),` `,n(`attr.antispam`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:s,onChange:e=>c(e.target.checked)}),` `,n(`attr.participantsHidden`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:l,onChange:e=>u(e.target.checked)}),` `,n(`attr.noforwards`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:d,onChange:e=>f(e.target.checked)}),` `,n(`attr.joinToSend`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>m(e.target.checked)}),` `,n(`attr.joinRequest`)]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`attr.slowmode`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,max:`86400`,value:h,onChange:e=>_(e.target.value)})]}),(0,H.jsx)(vt,{label:n(`attr.applySettings`),icon:(0,H.jsx)(V,{size:15}),tone:`warn`,path:`/api/actions/set-channel-settings`,payload:v,onDone:t})]})}function Dt({id:e,navigate:t}){let{t:n}=U(),[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)(`1000`),[p,m]=(0,g.useState)(()=>Ot(new Date(Date.now()+7*864e5))),[h,_]=(0,g.useState)(``);async function v(){c(!0),o(``);try{let t=await x.account(e);i(t),t.Restriction.Frozen&&(t.Restriction.Until&&m(Ot(new Date(t.Restriction.Until))),_(t.Restriction.AppealURL||``))}catch(e){o(b(e))}finally{c(!1)}}if((0,g.useEffect)(()=>{v()},[e]),a)return(0,H.jsx)(ut,{children:a});if(!r)return(0,H.jsx)(mt,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,H.jsx)(st,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,H.jsx)(N,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(lt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:tt(y)}),(0,H.jsxs)(`div`,{className:`entity-subtitle`,children:[et(y.Username)||n(`account.noUsername`),` · `,$e(y.Phone)||n(`account.noPhone`)]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,H.jsx)(q,{tone:`good`,children:n(`account.premium`)}):(0,H.jsx)(q,{children:n(`account.notPremium`)}),r.Verified?(0,H.jsx)(q,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(q,{children:n(`account.notVerified`)}),(0,H.jsx)(bt,{scam:r.Scam,fake:r.Fake}),y.Frozen?(0,H.jsx)(q,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,H.jsx)(q,{children:n(`account.accountActive`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Y,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,H.jsx)(Y,{label:n(`account.lastActive`),value:it(r.LastSeenAt)||`-`}),(0,H.jsx)(Y,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?it(y.PremiumUntil):n(`common.none`)}),(0,H.jsx)(Y,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,H.jsx)(Y,{label:n(`common.updatedAt`),value:rt(y.UpdatedAt)||`-`}),(0,H.jsx)(Y,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,H.jsx)(Y,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,H.jsx)(Y,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,H.jsx)(Y,{label:n(`account.freezeSince`),value:r.Restriction.Since?rt(r.Restriction.Since):n(`common.none`)}),(0,H.jsx)(Y,{label:n(`account.freezeUntil`),value:r.Restriction.Until?rt(r.Restriction.Until):n(`common.none`)}),(0,H.jsx)(Y,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,H.jsx)(Y,{label:n(`account.createdAt`),value:rt(y.CreatedAt)||`-`})]}),r.About&&(0,H.jsx)(`p`,{className:`about-text`,children:r.About}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,H.jsx)(yt,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(ft,{rows:r.AuditLogs})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,H.jsx)(vt,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,H.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!0,freeze_until:new Date(p).toISOString(),freeze_appeal_url:h.trim()}),onDone:v}),y.Frozen&&(0,H.jsx)(vt,{label:n(`account.unfreezeAccount`),icon:(0,H.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,H.jsxs)(`div`,{className:`action-stack`,children:[(0,H.jsx)(vt,{label:n(`account.setPremium`),icon:(0,H.jsx)(M,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:at(l)}),onDone:v}),(0,H.jsx)(vt,{label:n(`account.clearPremium`),icon:(0,H.jsx)(M,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,H.jsx)(vt,{label:n(`account.grantStars`),icon:(0,H.jsx)(Te,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:at(d)}),onDone:v}),(0,H.jsx)(vt,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,H.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:y.ID,verified:!r.Verified}),onDone:v})]}),(0,H.jsx)(xt,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-flags`,scam:r.Scam,fake:r.Fake,onDone:v}),(0,H.jsx)(`div`,{className:`dock-title`,children:n(`attr.attributes`)}),(0,H.jsx)(St,{id:y.ID,support:r.Support,onDone:v}),(0,H.jsx)(Ct,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-username`,current:y.Username,onDone:v}),(0,H.jsx)(wt,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-color`,onDone:v}),(0,H.jsx)(Tt,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-emoji-status`,onDone:v})]})})})}function Ot(e){return new Date(e.getTime()-e.getTimezoneOffset()*6e4).toISOString().slice(0,16)}function kt(e){return e.reduce((e,t)=>(e.devices+=t.DeviceCount,t.PremiumUntil>0&&(e.premium+=1),t.Frozen&&(e.frozen+=1),e),{devices:0,premium:0,frozen:0})}function At(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})}function jt({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`50`),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)({beforeID:0,beforeActiveUS:0}),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``);async function m(e=!1){d(!0),p(``);let t=new URLSearchParams({limit:i});n.trim()?t.set(`q`,n.trim()):e&&(t.set(`before_id`,String(c.beforeID)),t.set(`before_active_us`,String(c.beforeActiveUS)));try{let e=await x.accounts(t);s(e),l({beforeID:e.next_before_id,beforeActiveUS:e.next_before_active_us})}catch(e){p(b(e))}finally{d(!1)}}(0,g.useEffect)(()=>{m(!1)},[]);let h=kt(o?.rows??[]);return(0,H.jsxs)(st,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,H.jsx)(_e,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(ut,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(J,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,H.jsx)(J,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,H.jsx)(J,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,H.jsx)(J,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,H.jsx)(ct,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(ve,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(ve,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,H.jsx)(R,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`account.userID`)}),(0,H.jsx)(`th`,{children:t(`account.phone`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`common.name`)}),(0,H.jsx)(`th`,{children:t(`common.device`)}),(0,H.jsx)(`th`,{children:t(`account.lastActive`)}),(0,H.jsx)(`th`,{children:t(`account.premium`)}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`account.frozen`)}),(0,H.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:$e(n.Phone)}),(0,H.jsx)(`td`,{children:et(n.Username)}),(0,H.jsx)(`td`,{children:tt(n)}),(0,H.jsx)(`td`,{children:n.DeviceCount}),(0,H.jsx)(`td`,{children:rt(n.LastActiveAt)}),(0,H.jsx)(`td`,{children:n.PremiumUntil>0?(0,H.jsxs)(q,{tone:`good`,children:[t(`account.premium`),` `,it(n.PremiumUntil)]}):(0,H.jsx)(q,{children:t(`common.none`)})}),(0,H.jsxs)(`td`,{children:[n.Verified?(0,H.jsx)(q,{tone:`good`,children:t(`common.verified`)}):(0,H.jsx)(q,{children:t(`account.notVerified`)}),` `,(0,H.jsx)(bt,{scam:n.Scam,fake:n.Fake})]}),(0,H.jsx)(`td`,{children:n.Frozen?(0,H.jsx)(q,{tone:`danger`,children:t(`account.frozen`)}):(0,H.jsx)(q,{children:t(`common.normal`)})}),(0,H.jsx)(`td`,{children:rt(n.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(R,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,H.jsx)(pt,{colSpan:11})]})]})})]})}function X({id:e,navigate:t}){let{t:n}=U(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``);async function s(){o(``);try{i(await x.channel(e))}catch(e){o(b(e))}}if((0,g.useEffect)(()=>{s()},[e]),a)return(0,H.jsx)(ut,{children:a});if(!r)return(0,H.jsx)(mt,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,H.jsx)(st,{title:`${nt(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,H.jsx)(N,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(lt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,H.jsxs)(`div`,{className:`entity-subtitle`,children:[et(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(q,{children:nt(c,n)}),c.Verified?(0,H.jsx)(q,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(q,{children:n(`account.notVerified`)}),(0,H.jsx)(bt,{scam:c.Scam,fake:c.Fake}),c.Deleted?(0,H.jsx)(q,{tone:`danger`,children:n(`common.deleted`)}):(0,H.jsx)(q,{children:n(`common.valid`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Y,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,H.jsx)(Y,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,H.jsx)(Y,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,H.jsx)(Y,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,H.jsx)(Y,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,H.jsx)(Y,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,H.jsx)(Y,{label:n(`account.createdAt`),value:it(c.Date)||`-`}),(0,H.jsx)(Y,{label:n(`common.updatedAt`),value:rt(c.UpdatedAt)||`-`})]}),c.About&&(0,H.jsx)(`p`,{className:`about-text`,children:c.About}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(ft,{rows:r.AuditLogs})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,H.jsx)(ht,{value:r.ChannelJSON})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,H.jsx)(vt,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,H.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:c.ID,verified:!c.Verified}),onDone:s}),(0,H.jsx)(xt,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-flags`,scam:c.Scam,fake:c.Fake,onDone:s}),(0,H.jsx)(`div`,{className:`dock-title`,children:n(`attr.settings`)}),(0,H.jsx)(Et,{channel:c,onDone:s}),(0,H.jsx)(`div`,{className:`dock-title`,children:n(`attr.attributes`)}),(0,H.jsx)(Ct,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-username`,current:c.Username,onDone:s}),(0,H.jsx)(wt,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-color`,onDone:s}),(0,H.jsx)(Tt,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-emoji-status`,onDone:s})]})})})}function Mt({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`50`),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)({beforeID:0,beforeUpdatedUS:0}),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``);async function m(e=!1){d(!0),p(``);let t=new URLSearchParams({limit:i});n.trim()?t.set(`q`,n.trim()):e&&(t.set(`before_id`,String(c.beforeID)),t.set(`before_updated_us`,String(c.beforeUpdatedUS)));try{let e=await x.channels(t);s(e),l({beforeID:e.next_before_id,beforeUpdatedUS:e.next_before_updated_us})}catch(e){p(b(e))}finally{d(!1)}}(0,g.useEffect)(()=>{m(!1)},[]);let h=At(o?.rows??[]);return(0,H.jsxs)(st,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,H.jsx)(_e,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(ut,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(J,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,H.jsx)(J,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,H.jsx)(J,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,H.jsx)(J,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,H.jsx)(ct,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(ve,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(ve,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,H.jsx)(R,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`channel.channelID`)}),(0,H.jsx)(`th`,{children:t(`channel.kind`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`channel.title`)}),(0,H.jsx)(`th`,{children:t(`common.members`)}),(0,H.jsx)(`th`,{children:t(`common.admins`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:nt(n,t)}),(0,H.jsx)(`td`,{children:et(n.Username)}),(0,H.jsx)(`td`,{children:n.Title}),(0,H.jsx)(`td`,{children:n.ParticipantsCount}),(0,H.jsx)(`td`,{children:n.AdminsCount}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsxs)(`td`,{children:[n.Verified?(0,H.jsx)(q,{tone:`good`,children:t(`common.verified`)}):(0,H.jsx)(q,{children:t(`account.notVerified`)}),` `,(0,H.jsx)(bt,{scam:n.Scam,fake:n.Fake})]}),(0,H.jsx)(`td`,{children:rt(n.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(R,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,H.jsx)(pt,{colSpan:10})]})]})})]})}function Nt({id:e,navigate:t}){let{t:n}=U(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(!1);async function l(){c(!0),o(``);try{i(await x.bot(e))}catch(e){o(b(e))}finally{c(!1)}}if((0,g.useEffect)(()=>{l()},[e]),a)return(0,H.jsx)(ut,{children:a});if(!r)return(0,H.jsx)(mt,{label:n(s?`bots.loadingDetail`:`account.waitingData`)});let u=r.Bot;return(0,H.jsx)(st,{title:n(`bots.detailTitle`,{id:u.ID}),eyebrow:n(`bots.profile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/bots`),children:[(0,H.jsx)(N,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(lt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:u.FirstName||n(`bots.unnamed`)}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:et(u.Username)||n(`account.noUsername`)})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(q,{tone:u.System?`warn`:`neutral`,children:u.System?n(`bots.system`):n(`bots.user`)}),u.Verified?(0,H.jsx)(q,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(q,{children:n(`account.notVerified`)}),(0,H.jsx)(bt,{scam:u.Scam,fake:u.Fake})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Y,{label:n(`bots.botID`),value:String(u.ID),mono:!0}),(0,H.jsx)(Y,{label:n(`bots.owner`),value:u.OwnerUserID>0?`${u.OwnerUserID} ${et(r.OwnerUsername)}`.trim():n(`common.none`)}),(0,H.jsx)(Y,{label:n(`bots.type`),value:u.System?n(`bots.system`):n(`bots.user`)}),(0,H.jsx)(Y,{label:n(`common.updatedAt`),value:rt(u.UpdatedAt)||`-`}),(0,H.jsx)(Y,{label:n(`account.createdAt`),value:rt(u.CreatedAt)||`-`})]}),r.About&&(0,H.jsx)(`p`,{className:`about-text`,children:r.About}),r.Description&&r.Description.trim()!==r.About.trim()&&(0,H.jsx)(`p`,{className:`about-text`,children:r.Description}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(ft,{rows:r.AuditLogs})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`bots.actionDock`)}),(0,H.jsx)(`div`,{className:`action-stack`,children:(0,H.jsx)(vt,{label:u.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,H.jsx)(D,{size:15}),tone:`neutral`,path:`/api/actions/set-verified`,payload:()=>({user_id:u.ID,verified:!u.Verified}),onDone:l})}),(0,H.jsx)(xt,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-flags`,scam:u.Scam,fake:u.Fake,onDone:l}),(0,H.jsx)(`div`,{className:`dock-title`,children:n(`attr.attributes`)}),(0,H.jsx)(Ct,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-username`,current:u.Username,onDone:l}),(0,H.jsx)(wt,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-color`,onDone:l}),(0,H.jsx)(Tt,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-emoji-status`,onDone:l}),u.System?(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`bots.systemHint`)}):(0,H.jsxs)(`div`,{className:`danger-zone`,children:[(0,H.jsx)(vt,{label:n(`bots.delete`),icon:(0,H.jsx)(De,{size:15}),tone:`danger`,path:`/api/actions/delete-bot`,payload:()=>({bot_user_id:u.ID}),onDone:()=>t(`/bots`)}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`bots.deleteHint`)})]})]})})})}function Pt({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`50`),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(0),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(``),[y,S]=(0,g.useState)(``);async function C(e=!1){d(!0),p(``);let t=new URLSearchParams({limit:i});n.trim()?t.set(`q`,n.trim()):e&&t.set(`before_id`,String(c));try{let e=await x.bots(t);s(e),l(e.next_before_id)}catch(e){p(b(e))}finally{d(!1)}}(0,g.useEffect)(()=>{C(!1)},[]);let w=o?.rows??[],T=w.filter(e=>e.Verified).length,E=w.filter(e=>e.System).length;return(0,H.jsxs)(st,{title:t(`bots.pageTitle`),eyebrow:o?.listing===!1?t(`bots.queryResults`):t(`bots.recent`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>C(!1),disabled:u,children:[(0,H.jsx)(_e,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(ut,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(J,{label:t(`bots.currentPage`),value:String(w.length)}),(0,H.jsx)(J,{label:t(`common.verified`),value:String(T),tone:`good`}),(0,H.jsx)(J,{label:t(`bots.system`),value:String(E)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(`div`,{className:`section-head`,children:(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`h2`,{children:t(`bots.createTitle`)}),(0,H.jsx)(`p`,{children:t(`bots.createHint`)})]})}),(0,H.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`bots.ownerUserID`)}),(0,H.jsx)(`input`,{value:m,onChange:e=>h(e.target.value),type:`number`,min:`1`,placeholder:`123456789`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`bots.name`)}),(0,H.jsx)(`input`,{value:_,onChange:e=>v(e.target.value),placeholder:t(`bots.namePlaceholder`),maxLength:64})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`bots.username`)}),(0,H.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:`my_service_bot`})]})]}),(0,H.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,H.jsx)(`span`,{className:`bot-create-note`,children:t(`bots.usernameHint`)}),(0,H.jsx)(vt,{label:t(`bots.create`),icon:(0,H.jsx)(ge,{size:15}),tone:`neutral`,path:`/api/actions/create-bot`,payload:()=>({owner_user_id:at(m),name:_.trim(),username:y.trim().replace(/^@/,``)}),onDone:()=>C(!1)})]})]}),(0,H.jsx)(ct,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),C(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(ve,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`bots.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(ve,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>C(!0),disabled:u,children:[(0,H.jsx)(R,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`bots.botID`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`common.name`)}),(0,H.jsx)(`th`,{children:t(`bots.owner`)}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`bots.type`)}),(0,H.jsx)(`th`,{children:t(`account.createdAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[w.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:et(n.Username)||`-`}),(0,H.jsx)(`td`,{children:n.FirstName||`-`}),(0,H.jsx)(`td`,{className:`mono`,children:n.OwnerUserID>0?n.OwnerUserID:`-`}),(0,H.jsxs)(`td`,{children:[n.Verified?(0,H.jsxs)(q,{tone:`good`,children:[(0,H.jsx)(D,{size:12}),` `,t(`common.verified`)]}):(0,H.jsx)(q,{children:t(`account.notVerified`)}),` `,(0,H.jsx)(bt,{scam:n.Scam,fake:n.Fake})]}),(0,H.jsx)(`td`,{children:n.System?(0,H.jsx)(q,{tone:`warn`,children:t(`bots.system`)}):(0,H.jsx)(q,{children:t(`bots.user`)})}),(0,H.jsx)(`td`,{children:rt(n.CreatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/bots/${n.ID}`),children:[(0,H.jsx)(F,{size:14}),` `,t(`common.detail`),` `,(0,H.jsx)(R,{size:14})]})})]},n.ID)),w.length===0&&(0,H.jsx)(pt,{colSpan:8})]})]})})]})}var Ft=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),L(n[0],n[1],n[2])}function te(e,t){var n=ee(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),L(n[0],n[1],n[2])}function ne(e,t){var n=ee(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),L(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 re=function(e){g=!!e},ie=function(){return g},ae=function(e){_=e},oe=function(){return _},se=function(){return v},ce=function(e){E=e},le=function(){return E},ue=function(e){y=e};function z(e){return document.createElementNS(`http://www.w3.org/2000/svg`,e)}function de(e){"@babel/helpers - typeof";return de=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},de(e)}var B=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=z(`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 B.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`)},V.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)}},V.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`)))},V.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())},V.prototype.togglePause=function(e){e&&this.name!==e||(this.isPaused===!0?this.play():this.pause())},V.prototype.stop=function(e){e&&this.name!==e||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},V.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`))}},V.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`)},V.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)},V.prototype.playSegments=function(e,t){if(t&&(this.segments.length=0),be(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}(),Ce=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),we=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=Ce.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),Te=function(){function e(){return{addedLength:0,percents:p(`float32`,le()),lengths:p(`float32`,le())}}return we(8,e)}(),Ee=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=le(),a,o,s,c,l,u=0,d,f=[],p=[],m=Te.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=Ne(c.s),M=Ne(b),N=(e-y)/(v-y);H(r,Me(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 H(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 Ne(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 Pe(){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!==ke&&(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 Fe(e){var t;if(this.propType===`unidimensional`)t=e*this.mult,Ae(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 Ie(){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]=He.newElement()),a[r][0]=e,a[r][1]=t},Ue.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)},Ue.prototype.reverse=function(){var e=new Ue;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=Se.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 Ye(e){"@babel/helpers - typeof";return Ye=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},Ye(e)}var G={},Xe=`__[STANDALONE]__`,Ze=`__[ANIMATIONDATA]__`,Qe=``;function $e(e){s(e)}function et(){Xe===!0?xe.searchAnimations(Ze,Xe,Qe):xe.searchAnimations()}function tt(e){re(e)}function nt(e){ue(e)}function rt(e){return Xe===!0&&(e.animationData=JSON.parse(Ze)),xe.loadAnimation(e)}function it(e){if(typeof e==`string`)switch(e){case`high`:ce(200);break;default:case`medium`:ce(50);break;case`low`:ce(10);break}else!isNaN(e)&&e>1&&ce(e)}function at(){return typeof navigator<`u`}function ot(e,t){e===`expressions`&&ae(t)}function st(e){switch(e){case`propertyFactory`:return W;case`shapePropertyFactory`:return qe;case`matrix`:return Je;default:return null}}G.play=xe.play,G.pause=xe.pause,G.setLocationHref=$e,G.togglePause=xe.togglePause,G.setSpeed=xe.setSpeed,G.setDirection=xe.setDirection,G.stop=xe.stop,G.searchAnimations=et,G.registerAnimation=xe.registerAnimation,G.loadAnimation=rt,G.setSubframeRendering=tt,G.resize=xe.resize,G.goToAndStop=xe.goToAndStop,G.destroy=xe.destroy,G.setQuality=it,G.inBrowser=at,G.installPlugin=ot,G.freeze=xe.freeze,G.unfreeze=xe.unfreeze,G.setVolume=xe.setVolume,G.mute=xe.mute,G.unmute=xe.unmute,G.getRegisteredAnimations=xe.getRegisteredAnimations,G.useWebWorker=a,G.setIDPrefix=nt,G.__getFactory=st,G.version=`5.13.0`;function ct(){document.readyState===`complete`&&(clearInterval(dt),et())}function lt(e){for(var t=K.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},ft.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=Oe.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=Oe.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 Je,this.pre=new Je,this.appliedTransformations=0,this.initDynamicPropertyContainer(n||e),t.p&&t.p.s?(this.px=W.getProp(e,t.p.x,0,0,this),this.py=W.getProp(e,t.p.y,0,0,this),t.p.z&&(this.pz=W.getProp(e,t.p.z,0,0,this))):this.p=W.getProp(e,t.p||{k:[0,0,0]},1,0,this),t.rx){if(this.rx=W.getProp(e,t.rx,0,D,this),this.ry=W.getProp(e,t.ry,0,D,this),this.rz=W.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)},ht.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})},wt.prototype.split=function(e){if(e<=0)return[Ct(this.points[0]),this];if(e>=1)return[this,Ct(this.points[this.points.length-1])];var t=bt(this.points[0],this.points[1],e),n=bt(this.points[1],this.points[2],e),r=bt(this.points[2],this.points[3],e),i=bt(t,n,e),a=bt(n,r,e),o=bt(i,a,e);return[new wt(this.points[0],t,i,o,!0),new wt(o,a,r,this.points[3],!0)]};function Tt(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=xt(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}}wt.prototype.bounds=function(){return{x:Tt(this,0),y:Tt(this,1)}},wt.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 Et(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 Dt(e){var t=e.bez.split(.5);return[Et(t[0],e.t1,e.t),Et(t[1],e.t,e.t2)]}function Ot(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=Dt(e),s=Dt(t);kt(o[0],s[0],n+1,r,i,a),kt(o[0],s[1],n+1,r,i,a),kt(o[1],s[0],n+1,r,i,a),kt(o[1],s[1],n+1,r,i,a)}}wt.prototype.intersections=function(e,t,n){t===void 0&&(t=2),n===void 0&&(n=7);var r=[];return kt(Et(this,0,1),Et(e,0,1),0,t,r,n),r},wt.shapeSegment=function(e,t){var n=(t+1)%e.length();return new wt(e.v[t],e.o[t],e.i[n],e.v[n],!0)},wt.shapeSegmentInverted=function(e,t){var n=(t+1)%e.length();return new wt(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 vt(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 _t(e[0],t[0])&&_t(e[1],t[1])}function Pt(){}u([Y],Pt),Pt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amplitude=W.getProp(e,t.s,0,null,this),this.frequency=W.getProp(e,t.r,0,null,this),this.pointsType=W.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||_t(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([Y],qt),qt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amount=W.getProp(e,t.a,0,null,this),this.miterLimit=W.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=We.newElement();i.c=e.c;var a=e.length();e.c||--a;var o,s,c,l=[];for(o=0;o=0;--o)c=wt.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))},mn.prototype.show=function(){},mn.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},mn.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},mn.prototype.resume=function(){this._canPlay=!0},mn.prototype.setRate=function(e){this.audio.rate(e)},mn.prototype.volume=function(e){this._volumeMultiplier=e,this._previousVolume=e*this._volume,this.audio.volume(this._previousVolume)},mn.prototype.getBaseElement=function(){return null},mn.prototype.destroy=function(){},mn.prototype.sourceRectAtTime=function(){},mn.prototype.initExpressions=function(){};function hn(){}hn.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()},hn.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)}},hn.prototype.createCamera=function(){throw Error(`You're using a 3d camera. Try the html renderer.`)},hn.prototype.createAudio=function(e){return new mn(e,this.globalData,this)},hn.prototype.createFootage=function(e){return new pn(e,this.globalData,this)},hn.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)}vn.prototype.getMaskProperty=function(e){return this.viewData[e].prop},vn.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}},vn.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var yn=function(){var e={};e.createFilter=t,e.createAlphaToLuminanceFilter=n;function t(e,t){var n=z(`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=z(`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}(),bn=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}(),xn={},Sn=`filter_result_`;function Cn(e){var t,n=`SourceGraphic`,r=e.data.ef?e.data.ef.length:0,i=I(),a=yn.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},Rn.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 Gn(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([dn,_n,wn,kn,Tn,fn,En],Gn),Gn.prototype.initSecondaryElement=function(){},Gn.prototype.identityMatrix=new Je,Gn.prototype.buildExpressionInterface=function(){},Gn.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},Gn.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)}},Gn.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},qn.prototype.addEffect=function(e){this.effectsSequence.push(e),this.elem.addDynamicProperty(this)},qn.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},qn.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},qn.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,I,L=[];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),I=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=Se.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([Ve],r);function i(e,t,n){return new r(e,t,n)}return{getTextSelectorProp:i}}();function Yn(e,t,n){var r={propType:!1},i=W.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=Jn.getTextSelectorProp(e,t.s,n),this.s.t=t.s.t}function Xn(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)}Xn.prototype.searchProperties=function(){var e,t=this._textData.a.length,n,r=W.getProp;for(e=0;e=m+Ce||!x?(T=(m+Ce-g)/h.partialLength,ie=b.point[0]+(h.point[0]-b.point[0])*T,ae=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));re=f[u].an/2-f[u].add,a.translate(-re,0,0)}else re=f[u].an/2-f[u].add,a.translate(-re,0,0),a.translate(-n[0]*f[u].an*.005,-n[1]*A*.01,0);for(P=0;Pe?this.textSpans[e].span:z(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=z(`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 ir(x.data,this.globalData,this);else{var w=Qn;x.data&&x.data.shapes&&(w=this.buildShapeData(x.data,n.finalSize)),C=new Gn(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))}},rr.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=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(t);var n=lr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(n),this.data.tt>=3&&!document._isProxy&&lr.loadLumaCanvas()}this.canvasContext=this.globalData.canvasContext,this.transformCanvas=this.globalData.transformCanvas,this.renderableEffectsManager=new dr(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 fr(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(gn.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=lr.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=mr[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 Je},pr.prototype.hide=pr.prototype.hideElement,pr.prototype.show=pr.prototype.showElement;function hr(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=qe.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()},gr.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},Z.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()}}},Z.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()}},Z.prototype.checkPendingElements=function(){for(;this.pendingElements.length;)this.pendingElements.pop().checkParenting()},Z.prototype.hide=function(){this.animationItem.container.style.display=`none`},Z.prototype.show=function(){this.animationItem.container.style.display=`block`};function br(){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 xr(){this.stack=[],this.cArrPos=0,this.cTr=new Je;var e,t=15;for(e=0;e=0;--t)(this.completeLayers||this.elements[t])&&this.elements[t].renderFrame()},Sr.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},Sr.prototype.createComp=function(e){return new Sr(e,this.globalData,this)};function Cr(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 xr,this.elements=[],this.pendingElements=[],this.transformMat=new Je,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([Z],Cr),Cr.prototype.createComp=function(e){return new Sr(e,this.globalData,this)},_e(`canvas`,Cr),J.registerModifier(`tm`,ft),J.registerModifier(`pb`,pt),J.registerModifier(`rp`,ht),J.registerModifier(`rd`,gt),J.registerModifier(`zz`,Pt),J.registerModifier(`op`,qt),G}))}))(),1);function It({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=Ft.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,H.jsx)(`div`,{className:n,ref:a,onMouseEnter:s,onMouseLeave:c})}function Lt(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function Rt(e){let t=e.toLowerCase();return t.includes(`tgsticker`)||t.includes(`lottie`)||t.includes(`json`)}function zt({row:e}){let[t,n]=(0,g.useState)(!Rt(e.MimeType));return(0,g.useEffect)(()=>{n(!Rt(e.MimeType))},[e.DocumentID,e.MimeType]),t?(0,H.jsx)(`div`,{className:`emoji-glyph`,children:e.Alt||`🙂`}):(0,H.jsx)(It,{className:`emoji-anim`,cacheKey:e.DocumentID,loader:()=>x.emojiAnimation(e.DocumentID),onError:()=>n(!0)})}function Bt({row:e}){let{t}=U(),[n,r]=(0,g.useState)(!1);async function i(){try{await navigator.clipboard.writeText(e.DocumentID),r(!0),setTimeout(()=>r(!1),1200)}catch{}}return(0,H.jsxs)(`div`,{className:`emoji-card`,children:[(0,H.jsx)(`div`,{className:`emoji-preview`,children:(0,H.jsx)(zt,{row:e})}),(0,H.jsxs)(`div`,{className:`emoji-meta`,children:[(0,H.jsx)(`span`,{className:`emoji-alt`,children:e.Alt||`—`}),(0,H.jsxs)(`button`,{className:`emoji-id`,type:`button`,onClick:i,title:t(`emoji.copyID`),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.DocumentID}),n?(0,H.jsx)(L,{size:12}):(0,H.jsx)(ne,{size:12})]}),(0,H.jsxs)(`span`,{className:`emoji-sub`,children:[e.SetTitle||t(`emoji.noSet`),` · `,Lt(e.Size)]})]})]})}function Vt(){let{t:e}=U(),[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(null),[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 n=new URLSearchParams;t.trim()?n.set(`q`,t.trim()):e&&n.set(`before_id`,String(a));try{let e=await x.emoji(n);i(e),o(e.next_before_id)}catch(e){u(b(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d(!1)},[]);let f=r?.rows??[];return(0,H.jsxs)(st,{title:e(`emoji.pageTitle`),eyebrow:r?.listing===!1?e(`emoji.queryResults`):e(`emoji.recent`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>d(!1),disabled:s,children:[(0,H.jsx)(_e,{size:15}),` `,e(`common.refresh`)]}),children:[l&&(0,H.jsx)(ut,{children:l}),(0,H.jsx)(`div`,{className:`metric-row`,children:(0,H.jsx)(J,{label:e(`emoji.currentPage`),value:String(f.length)})}),(0,H.jsx)(ct,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),d(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(ve,{size:15}),(0,H.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:e(`emoji.searchPlaceholder`)})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:s,children:[s?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(ve,{size:15}),` `,e(`common.search`)]}),r?.listing&&r.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>d(!0),disabled:s,children:[(0,H.jsx)(R,{size:15}),` `,e(`messages.nextPage`)]})]})}),(0,H.jsx)(`p`,{className:`about-text`,children:e(`emoji.hint`)}),f.length===0?(0,H.jsx)(`div`,{className:`empty-panel`,children:e(`common.noResults`)}):(0,H.jsx)(`div`,{className:`emoji-grid`,children:f.map(e=>(0,H.jsx)(Bt,{row:e},e.DocumentID))})]})}function Ht({navigate:e}){let{t}=U();return(0,H.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,H.jsxs)(`section`,{className:`overview-band`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,H.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,H.jsxs)(`div`,{className:`overview-metrics`,children:[(0,H.jsx)(dt,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,H.jsx)(dt,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,H.jsx)(dt,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,H.jsxs)(`div`,{className:`command-grid`,children:[(0,H.jsx)(Ut,{icon:(0,H.jsx)(Ae,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,H.jsx)(Ut,{icon:(0,H.jsx)(Se,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,H.jsx)(Ut,{icon:(0,H.jsx)(B,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,H.jsxs)(`section`,{className:`work-strip`,children:[(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(k,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(le,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(te,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(ae,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function Ut({icon:e,title:t,text:n,href:r,navigate:i}){return(0,H.jsxs)(G,{className:`launcher`,href:r,navigate:i,children:[(0,H.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,H.jsxs)(`span`,{className:`launcher-copy`,children:[(0,H.jsx)(`strong`,{children:t}),(0,H.jsx)(`span`,{children:n})]}),(0,H.jsx)(R,{size:16})]})}function Wt({channelID:e,msgID:t,navigate:n}){let{t:r}=U(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``);async function c(){s(``);try{a(await x.groupMessage(e,t))}catch(e){s(b(e))}}if((0,g.useEffect)(()=>{c()},[e,t]),o)return(0,H.jsx)(ut,{children:o});if(!i)return(0,H.jsx)(mt,{label:r(`common.loading`)});let l=i.Message;return(0,H.jsx)(st,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,H.jsx)(N,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:it(l.Date)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,H.jsx)(q,{tone:`danger`,children:r(`common.deleted`)}):(0,H.jsx)(q,{children:r(`common.survived`)}),l.Pinned&&(0,H.jsx)(q,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,H.jsx)(q,{children:r(`messages.channelPost`)}),(0,H.jsxs)(q,{children:[`pts `,l.PTS]})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Y,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,H.jsx)(Y,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,H.jsx)(Y,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,H.jsx)(Y,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,H.jsx)(ht,{value:i.MessageJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,H.jsx)(ht,{value:i.ChannelJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.count`)}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.messageId`)}),(0,H.jsx)(`th`,{children:r(`common.sender`)}),(0,H.jsx)(`th`,{children:r(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.PTSCount}),(0,H.jsx)(`td`,{children:e.Type}),(0,H.jsx)(`td`,{children:e.MessageID}),(0,H.jsx)(`td`,{children:e.SenderUserID}),(0,H.jsx)(`td`,{children:it(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,H.jsx)(pt,{colSpan:6})]})]})})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:r(`messages.eventJson`)}),(0,H.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,H.jsx)(ht,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,H.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function Gt({label:e,value:t,onChange:n}){let{t:r}=U(),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);let e=new URLSearchParams({limit:`20`});i.trim()&&e.set(`q`,i.trim());try{s((await x.accounts(e)).rows)}catch(e){d(b(e))}finally{l(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,H.jsxs)(`div`,{className:`entity-picker`,children:[(0,H.jsxs)(`div`,{className:`picker-head`,children:[(0,H.jsx)(`span`,{children:e}),t?(0,H.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,H.jsx)(je,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,H.jsxs)(`div`,{className:`selected-entity`,children:[(0,H.jsx)(L,{size:15}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:tt(t)}),(0,H.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,H.jsx)(`span`,{children:et(t.Username)||$e(t.Phone)||`-`})]}):null,(0,H.jsxs)(`div`,{className:`picker-search`,children:[(0,H.jsx)(ve,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,H.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,H.jsx)(`div`,{className:`picker-error`,children:u}),(0,H.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,H.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.ID}),(0,H.jsx)(`strong`,{children:tt(e)}),(0,H.jsx)(`span`,{children:et(e.Username)||$e(e.Phone)||`-`}),e.Verified?(0,H.jsx)(q,{tone:`good`,children:r(`picker.verified`)}):(0,H.jsx)(q,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,H.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function Kt({label:e,value:t,onChange:n}){let{t:r}=U(),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);let e=new URLSearchParams({limit:`20`});i.trim()&&e.set(`q`,i.trim());try{s((await x.channels(e)).rows)}catch(e){d(b(e))}finally{l(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,H.jsxs)(`div`,{className:`entity-picker`,children:[(0,H.jsxs)(`div`,{className:`picker-head`,children:[(0,H.jsx)(`span`,{children:e}),t?(0,H.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,H.jsx)(je,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,H.jsxs)(`div`,{className:`selected-entity`,children:[(0,H.jsx)(L,{size:15}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:t.Title||`-`}),(0,H.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,H.jsx)(`span`,{children:et(t.Username)||nt(t,r)})]}):null,(0,H.jsxs)(`div`,{className:`picker-search`,children:[(0,H.jsx)(ve,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,H.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,H.jsx)(`div`,{className:`picker-error`,children:u}),(0,H.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,H.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.ID}),(0,H.jsx)(`strong`,{children:e.Title||`-`}),(0,H.jsx)(`span`,{children:et(e.Username)||nt(e,r)}),e.Verified?(0,H.jsx)(q,{tone:`good`,children:r(`picker.verified`)}):(0,H.jsx)(q,{children:nt(e,r)})]},e.ID)),o.length===0&&!c?(0,H.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function qt({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(`100`),[u,d]=(0,g.useState)(null),[f,p]=(0,g.useState)(``);async function m(e=!1){if(p(``),!n){p(t(`messages.selectChannel`));return}let r=new URLSearchParams({channel_id:String(n.ID),limit:c});if(e&&u?.rows.length){let e=u.rows[u.rows.length-1];r.set(`before_date`,String(e.Date)),r.set(`before_id`,String(e.ID)),a(String(e.Date)),s(String(e.ID))}else i&&r.set(`before_date`,i),o&&r.set(`before_id`,o);try{d(await x.groupMessages(r))}catch(e){p(b(e))}}function h(e){r(e),a(``),s(``),d(null)}let _=u?.rows??[];return(0,H.jsxs)(st,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,H.jsx)(ut,{children:f}),(0,H.jsxs)(ct,{children:[(0,H.jsx)(`div`,{className:`message-selector-grid single`,children:(0,H.jsx)(Kt,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,H.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,H.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,H.jsx)(ve,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,H.jsx)(R,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(J,{label:t(`messages.currentPage`),value:String(_.length)}),(0,H.jsx)(J,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,H.jsx)(J,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,H.jsx)(J,{label:t(`messages.channelGroup`),value:n?`${n.Title||nt(n,t)} (${n.ID})`:`-`})]}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.messageId`)}),(0,H.jsx)(`th`,{children:t(`common.time`)}),(0,H.jsx)(`th`,{children:t(`common.sender`)}),(0,H.jsx)(`th`,{children:`From Peer`}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.views`)}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`messages.body`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[_.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:it(n.Date)}),(0,H.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,H.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.ViewsCount}),(0,H.jsx)(`td`,{children:n.Deleted?(0,H.jsx)(q,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,H.jsx)(q,{tone:`warn`,children:t(`messages.pinned`)}):(0,H.jsx)(q,{children:t(`common.survived`)})}),(0,H.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(R,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,H.jsx)(pt,{colSpan:9})]})]})})]})}function Jt({ownerUserID:e,msgID:t,navigate:n}){let{t:r}=U(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``);async function c(){s(``);try{a(await x.message(e,t))}catch(e){s(b(e))}}if((0,g.useEffect)(()=>{c()},[e,t]),o)return(0,H.jsx)(ut,{children:o});if(!i)return(0,H.jsx)(mt,{label:r(`common.loading`)});let l=i.Message;return(0,H.jsx)(st,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,H.jsx)(N,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,H.jsx)(lt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:it(l.Date)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,H.jsx)(q,{tone:`danger`,children:r(`common.deleted`)}):(0,H.jsx)(q,{children:r(`common.survived`)}),(0,H.jsxs)(q,{children:[`pts `,l.PTS]}),(0,H.jsx)(q,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Y,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,H.jsx)(Y,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,H.jsx)(Y,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,H.jsx)(Y,{label:r(`common.time`),value:it(l.Date)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,H.jsx)(ht,{value:i.MessageJSON})]}),(0,H.jsxs)(`div`,{className:`raw-grid`,children:[(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,H.jsx)(ht,{value:i.DialogJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,H.jsx)(ht,{value:i.PrivateJSON})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.count`)}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.PTSCount}),(0,H.jsx)(`td`,{children:e.Type}),(0,H.jsx)(`td`,{children:it(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,H.jsx)(pt,{colSpan:4})]})]})})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`ID`}),(0,H.jsx)(`th`,{children:r(`account.userID`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.status`)}),(0,H.jsx)(`th`,{children:r(`messages.attempts`)}),(0,H.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.ID}),(0,H.jsx)(`td`,{children:e.TargetUserID}),(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.EventType}),(0,H.jsx)(`td`,{children:e.Status}),(0,H.jsx)(`td`,{children:e.Attempts}),(0,H.jsx)(`td`,{children:rt(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,H.jsx)(pt,{colSpan:7})]})]})})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,H.jsx)(vt,{label:r(`messages.deleteThis`),icon:(0,H.jsx)(De,{size:15}),path:`/api/actions/delete-messages`,payload:()=>({owner_user_id:l.OwnerUserID,peer_id:l.PeerID,ids:[l.BoxID],revoke:!0}),onDone:c})]})})})}function Yt({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(`100`),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!0),[_,v]=(0,g.useState)(!1),[y,S]=(0,g.useState)(``),[C,w]=(0,g.useState)(`1`),[T,E]=(0,g.useState)(null),[D,O]=(0,g.useState)(``);async function k(e=!1){if(O(``),!n||!i){O(t(`messages.selectPrivatePeers`));return}let r=new URLSearchParams({owner_user_id:String(n.ID),peer_id:String(i.ID),limit:u});if(e&&T?.rows.length){let e=T.rows[T.rows.length-1];r.set(`before_date`,String(e.Date)),r.set(`before_id`,String(e.BoxID)),s(String(e.Date)),l(String(e.BoxID))}else o&&r.set(`before_date`,o),c&&r.set(`before_id`,c);try{E(await x.messages(r))}catch(e){O(b(e))}}function A(e){r(e),s(``),l(``),E(null)}function j(e){a(e),s(``),l(``),E(null)}return(0,H.jsxs)(st,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[D&&(0,H.jsx)(ut,{children:D}),(0,H.jsxs)(ct,{children:[(0,H.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,H.jsx)(Gt,{label:t(`messages.ownerUser`),value:n,onChange:A}),(0,H.jsx)(Gt,{label:t(`messages.peerUser`),value:i,onChange:j})]}),(0,H.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),k(!1)},children:[(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,H.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,H.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,H.jsx)(ve,{size:15}),` `,t(`messages.searchMessages`)]}),T?.rows.length?(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>k(!0),children:[(0,H.jsx)(R,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(J,{label:t(`messages.currentPage`),value:String(T?.rows.length??0)}),(0,H.jsx)(J,{label:t(`messages.deleted`),value:String((T?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,H.jsx)(J,{label:t(`messages.outgoing`),value:String((T?.rows??[]).filter(e=>e.Outgoing).length)}),(0,H.jsx)(J,{label:t(`messages.ownerPeer`),value:n&&i?`${tt(n)} / ${tt(i)}`:`-`})]}),(0,H.jsxs)(`div`,{className:`operation-row`,children:[(0,H.jsxs)(`div`,{className:`operation-box`,children:[(0,H.jsxs)(`div`,{className:`operation-title`,children:[(0,H.jsx)(De,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,H.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,H.jsx)(vt,{path:`/api/actions/delete-messages`,label:t(`messages.previewDelete`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,ids:ot(f,t(`messages.msgIDsInvalid`)),revoke:m})})]}),(0,H.jsxs)(`div`,{className:`operation-box`,children:[(0,H.jsxs)(`div`,{className:`operation-title`,children:[(0,H.jsx)(ce,{size:15}),` `,t(`messages.clearHistory`)]}),(0,H.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,H.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,H.jsx)(vt,{path:`/api/actions/delete-history`,label:t(`messages.previewClearHistory`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,max_id:at(y),max_batches:at(C),just_clear:_,revoke:m})})]})]}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.messageId`)}),(0,H.jsx)(`th`,{children:t(`common.time`)}),(0,H.jsx)(`th`,{children:t(`common.sender`)}),(0,H.jsx)(`th`,{children:t(`messages.direction`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`messages.body`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[T?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,H.jsx)(`td`,{children:it(n.Date)}),(0,H.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,H.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.Deleted?(0,H.jsx)(q,{tone:`danger`,children:t(`common.deleted`)}):(0,H.jsx)(q,{children:t(`common.survived`)})}),(0,H.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,H.jsx)(R,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!T||T.rows.length===0)&&(0,H.jsx)(pt,{colSpan:8})]})]})})]})}var Xt=0,Zt=e=>`${e}-${++Xt}`,Qt=[{center:`#6f5bea`,edge:`#34278f`,pattern:`#a89df5`,text:`#ffffff`},{center:`#32a86b`,edge:`#17613e`,pattern:`#8ee0b3`,text:`#ffffff`},{center:`#df8d2f`,edge:`#8c421e`,pattern:`#ffd08a`,text:`#ffffff`},{center:`#d95878`,edge:`#7b2944`,pattern:`#f5a1b6`,text:`#ffffff`}];function $t(e){if(!e.length)return e;let t=Math.floor(1e3/e.length),n=1e3%e.length;return e.map((e,r)=>({...e,rarity:String(t+ +(r({key:Zt(e),name:``,rarity:`1`,sortOrder:String(t),file:null,animation:null,fileError:``});function tn(e){let t=e.reduce((e,t)=>{let n=Number(t.backdropID);return Number.isInteger(n)?Math.max(e,n):e},0)+1,n=Qt[e.length%Qt.length];return{key:Zt(`backdrop`),name:``,backdropID:String(t),rarity:`1`,sortOrder:String(e.length),...n}}var nn=e=>$t([en(e,0),en(e,1)]),rn=()=>{let e=tn([]);return $t([e,tn([e])])};function an({data:e,compact:t=!1}){let n=(0,g.useRef)(null);return(0,g.useEffect)(()=>{if(!n.current)return;let t=Ft.default.loadAnimation({container:n.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)});return()=>t.destroy()},[e]),(0,H.jsx)(`div`,{className:`collectible-animation ${t?`compact`:``}`,ref:n})}function on({giftID:e,attribute:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(!1);return(0,g.useEffect)(()=>{let n=!1;return a(!1),x.giftCollectibleAnimation(e,t.kind,t.id).then(e=>{n||r(e)}).catch(()=>{n||a(!0)}),()=>{n=!0}},[e,t.id,t.kind]),i?(0,H.jsx)(`div`,{className:`collectible-animation compact failed`,children:`!`}):n?(0,H.jsx)(an,{data:n,compact:!0}):(0,H.jsx)(`div`,{className:`collectible-animation compact loading`,children:(0,H.jsx)(A,{className:`spin`,size:15})})}async function sn(e){let t=new Uint8Array(await e.arrayBuffer()),n=t;if(t.length>=2&&t[0]===31&&t[1]===139){if(!(`DecompressionStream`in window))throw Error(`This browser cannot preview TGS files`);let e=new Blob([t]).stream().pipeThrough(new DecompressionStream(`gzip`));n=new Uint8Array(await new Response(e).arrayBuffer())}let r=JSON.parse(new TextDecoder().decode(n));if(!r||typeof r!=`object`||Array.isArray(r))throw Error(`Invalid Lottie JSON`);return r}var cn=e=>Number.parseInt(e.replace(`#`,``),16),ln=e=>e.rarity_kind===`permille`?`${e.rarity_permille}‰`:e.rarity_kind;function un({gift:e,onClose:t,onPublished:n}){let{t:r}=U(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(!0),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(null),[m,h]=(0,g.useState)(`100`),[_,v]=(0,g.useState)(`1000`),[y,S]=(0,g.useState)(`gift-${e.GiftID}`),[C,w]=(0,g.useState)(``),[T,E]=(0,g.useState)(()=>nn(`model`)),[D,O]=(0,g.useState)(()=>nn(`pattern`)),[j,N]=(0,g.useState)(rn);(0,g.useEffect)(()=>{let t=!1;return x.giftCollectibles(e.GiftID).then(n=>{t||(a(n),n.found&&(h(String(n.upgrade_stars??100)),v(String(n.supply_total??1e3)),S(n.slug_prefix??`gift-${e.GiftID}`)))}).catch(e=>d(b(e))).finally(()=>{t||s(!1)}),()=>{t=!0}},[e.GiftID]);let P=(0,g.useMemo)(()=>({models:T.reduce((e,t)=>e+Number(t.rarity||0),0),patterns:D.reduce((e,t)=>e+Number(t.rarity||0),0),backdrops:j.reduce((e,t)=>e+Number(t.rarity||0),0)}),[T,D,j]),F=()=>p(null),I=(e,t,n)=>{(e===`models`?E:O)(e=>e.map(e=>e.key===t?{...e,...n}:e)),F()};async function L(e,t,n){if(I(e,t.key,{file:n,animation:null,fileError:``}),n)try{let r=await sn(n);I(e,t.key,{animation:r,fileError:``})}catch(n){I(e,t.key,{animation:null,fileError:b(n)})}}function ee(e,t=``){if(!C.trim())throw Error(r(`action.reasonRequired`));if(T.length<2||D.length<2||j.length<2)throw Error(r(`collectibles.minimumAttributes`));let n=j.map(e=>Number(e.backdropID));if(new Set(n).size!==n.length)throw Error(r(`collectibles.duplicateBackdropID`));for(let e of[...T,...D])if(!e.file)throw Error(r(`collectibles.fileRequired`));let i=new FormData,a=e=>e.map(e=>({name:e.name.trim(),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),file_key:e.key}));i.set(`metadata`,JSON.stringify({command_id:t,reason:C.trim(),confirm:e,upgrade_stars:m,supply_total:Number(_),slug_prefix:y.trim().toLowerCase(),models:a(T),patterns:a(D),backdrops:j.map(e=>({name:e.name.trim(),backdrop_id:Number(e.backdropID),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),center_color:cn(e.center),edge_color:cn(e.edge),pattern_color:cn(e.pattern),text_color:cn(e.text)}))}));for(let e of[...T,...D])i.set(e.key,e.file,e.file.name);return i}async function R(){l(!0),d(``),p(null);try{p(await x.publishGiftCollectibles(e.GiftID,ee(!1)))}catch(e){d(b(e))}finally{l(!1)}}async function te(){if(f){l(!0),d(``);try{await x.publishGiftCollectibles(e.GiftID,ee(!0,f.command_id)),n(),t()}catch(e){d(b(e))}finally{l(!1)}}}let ne=(e,t,n)=>(0,H.jsxs)(`section`,{className:`collectible-section`,children:[(0,H.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.${e}`)}),(0,H.jsx)(`span`,{children:r(`collectibles.rarityHint`)})]}),(0,H.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,H.jsxs)(q,{tone:P[e]>0?`good`:`neutral`,children:[P[e],`‰`]}),(0,H.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{n($t([...t,en(e===`models`?`model`:`pattern`,t.length)])),F()},children:[(0,H.jsx)(ge,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,H.jsx)(`div`,{className:`collectible-rows`,children:t.map((i,a)=>(0,H.jsxs)(`div`,{className:`collectible-row animated`,children:[(0,H.jsx)(`div`,{className:`collectible-row-index`,children:a+1}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`common.name`)}),(0,H.jsx)(`input`,{value:i.name,maxLength:128,onChange:t=>I(e,i.key,{name:t.target.value})})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:i.rarity,onChange:t=>I(e,i.key,{rarity:t.target.value})})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:i.sortOrder,onChange:t=>I(e,i.key,{sortOrder:t.target.value})})]}),(0,H.jsxs)(`label`,{className:`collectible-file`,children:[(0,H.jsx)(`span`,{children:r(`gifts.animation`)}),(0,H.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:t=>void L(e,i,t.target.files?.[0]??null)}),(0,H.jsxs)(`em`,{children:[(0,H.jsx)(ie,{size:13}),i.file?.name??r(`gifts.chooseFile`)]})]}),(0,H.jsx)(`div`,{className:`collectible-inline-preview`,children:i.animation?(0,H.jsx)(an,{data:i.animation,compact:!0}):(0,H.jsx)(M,{size:16})}),(0,H.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:t.length<=2,onClick:()=>{n($t(t.filter(e=>e.key!==i.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,H.jsx)(De,{size:14})}),i.fileError&&(0,H.jsx)(`span`,{className:`collectible-file-error`,children:i.fileError})]},i.key))})]});return(0,_t.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal collectible-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":r(`collectibles.title`,{id:e.GiftID}),children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:r(`collectibles.eyebrow`)}),(0,H.jsx)(`h2`,{children:r(`collectibles.title`,{id:e.GiftID})}),(0,H.jsx)(`p`,{children:e.Title||`Gift #${e.GiftID}`})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:c,"aria-label":r(`action.close`),children:(0,H.jsx)(je,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body collectible-modal-body`,children:[o?(0,H.jsxs)(`div`,{className:`collectible-loading`,children:[(0,H.jsx)(A,{className:`spin`}),r(`common.loading`)]}):i?.found?(0,H.jsxs)(`section`,{className:`collectible-active`,children:[(0,H.jsxs)(`div`,{className:`collectible-active-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(oe,{size:18}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.activeRevision`,{revision:i.revision??0})}),(0,H.jsxs)(`span`,{children:[i.slug_prefix,` · ⭐ `,i.upgrade_stars,` · `,i.issued,` / `,i.supply_total]})]})]}),(0,H.jsx)(q,{tone:`good`,children:r(`collectibles.published`)})]}),(0,H.jsxs)(`div`,{className:`collectible-active-grid`,children:[[...i.models??[],...i.patterns??[]].map(t=>(0,H.jsxs)(`article`,{children:[(0,H.jsx)(on,{giftID:e.GiftID,attribute:t}),(0,H.jsxs)(`div`,{children:[(0,H.jsxs)(`strong`,{children:[t.name,t.crafted&&(0,H.jsx)(q,{children:`crafted`})]}),(0,H.jsxs)(`span`,{children:[r(`collectibles.${t.kind}`),` · `,ln(t)]})]})]},`${t.kind}-${t.id}`)),(i.backdrops??[]).map(e=>(0,H.jsxs)(`article`,{children:[(0,H.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, #${(e.center_color??0).toString(16).padStart(6,`0`)}, #${(e.edge_color??0).toString(16).padStart(6,`0`)})`,color:`#${(e.text_color??16777215).toString(16).padStart(6,`0`)}`},children:`Aa`}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e.name}),(0,H.jsxs)(`span`,{children:[r(`collectibles.backdrop`),` · `,ln(e)]})]})]},`backdrop-${e.id}`))]})]}):(0,H.jsxs)(`div`,{className:`collectible-empty`,children:[(0,H.jsx)(oe,{size:22}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.noPool`)}),(0,H.jsx)(`span`,{children:r(`collectibles.noPoolHint`)})]})]}),(0,H.jsxs)(`section`,{className:`collectible-definition`,children:[(0,H.jsxs)(`div`,{className:`collectible-definition-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.publishNew`)}),(0,H.jsx)(`span`,{children:r(`collectibles.immutableHint`)})]}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,H.jsx)(`span`,{children:`TGS`}),(0,H.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,H.jsxs)(`div`,{className:`gift-fields-grid collectible-main-fields`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.upgradeStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:m,onChange:e=>{h(e.target.value),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.supply`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:_,onChange:e=>{v(e.target.value),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.slug`)}),(0,H.jsx)(`input`,{value:y,maxLength:48,onChange:e=>{S(e.target.value.toLowerCase()),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.reason`)}),(0,H.jsx)(`input`,{value:C,maxLength:1e3,placeholder:r(`gifts.reasonPlaceholder`),onChange:e=>w(e.target.value)})]})]}),ne(`models`,T,E),ne(`patterns`,D,O),(0,H.jsxs)(`section`,{className:`collectible-section`,children:[(0,H.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.backdrops`)}),(0,H.jsx)(`span`,{children:r(`collectibles.colorHint`)})]}),(0,H.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,H.jsxs)(q,{tone:P.backdrops>0?`good`:`neutral`,children:[P.backdrops,`‰`]}),(0,H.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{N($t([...j,tn(j)])),F()},children:[(0,H.jsx)(ge,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,H.jsx)(`div`,{className:`collectible-rows`,children:j.map((e,t)=>(0,H.jsxs)(`div`,{className:`collectible-row backdrop`,children:[(0,H.jsx)(`div`,{className:`collectible-row-index`,children:t+1}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`common.name`)}),(0,H.jsx)(`input`,{value:e.name,maxLength:128,onChange:t=>{N(j.map(n=>n.key===e.key?{...n,name:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.backdropID`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:e.backdropID,onChange:t=>{N(j.map(n=>n.key===e.key?{...n,backdropID:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:e.rarity,onChange:t=>{N(j.map(n=>n.key===e.key?{...n,rarity:t.target.value}:n)),F()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:e.sortOrder,onChange:t=>{N(j.map(n=>n.key===e.key?{...n,sortOrder:t.target.value}:n)),F()}})]}),[`center`,`edge`,`pattern`,`text`].map(t=>(0,H.jsxs)(`label`,{className:`collectible-color`,children:[(0,H.jsx)(`span`,{children:r(`collectibles.color.${t}`)}),(0,H.jsx)(`input`,{type:`color`,value:e[t],onChange:n=>{N(j.map(r=>r.key===e.key?{...r,[t]:n.target.value}:r)),F()}})]},t)),(0,H.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, ${e.center}, ${e.edge})`,color:e.text},children:`Aa`}),(0,H.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:j.length<=2,onClick:()=>{N($t(j.filter(t=>t.key!==e.key))),F()},"aria-label":r(`collectibles.remove`),children:(0,H.jsx)(De,{size:14})})]},e.key))})]})]}),u&&(0,H.jsx)(ut,{children:u}),f&&(0,H.jsxs)(`div`,{className:`gift-validation`,children:[(0,H.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,H.jsx)(k,{size:17}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.validationReady`)}),(0,H.jsx)(`span`,{children:r(`collectibles.validationHint`)})]})]}),(0,H.jsx)(`pre`,{children:JSON.stringify(f.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:c,children:r(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:R,disabled:c,children:[c?(0,H.jsx)(A,{className:`spin`,size:15}):(0,H.jsx)(Se,{size:15}),r(`gifts.validate`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:te,disabled:c||!f,children:[(0,H.jsx)(Oe,{size:15}),r(`collectibles.publish`)]})]})]})}),document.body)}function dn(e){return e.model_count+e.pattern_count+e.backdrop_count}function fn(e){let t=Number(e);return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function pn({giftID:e,revision:t,compact:n=!1}){let r=(0,g.useRef)(null),i=(0,g.useRef)(null),[a,o]=(0,g.useState)(!0),[s,c]=(0,g.useState)(``);(0,g.useEffect)(()=>{let t=!1;return x.giftAnimation(e).then(e=>{t||!r.current||(i.current?.destroy(),i.current=Ft.default.loadAnimation({container:r.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(e=>c(b(e))),()=>{t=!0,i.current?.destroy(),i.current=null}},[e,t]);function l(){i.current&&(a?i.current.pause():i.current.play(),o(!a))}return(0,H.jsxs)(`div`,{className:`gift-animation-shell ${n?`compact`:``}`,children:[(0,H.jsx)(`div`,{className:`gift-animation`,ref:r,children:s&&(0,H.jsx)(`span`,{children:s})}),(0,H.jsx)(`button`,{className:`gift-play`,type:`button`,onClick:l,"aria-label":a?`Pause`:`Play`,children:a?(0,H.jsx)(me,{size:14}):(0,H.jsx)(he,{size:14})})]})}function mn({sourceGiftID:e}){let t=(0,g.useRef)(null);return(0,g.useEffect)(()=>{let n=!1,r=null;return x.officialGiftAnimation(e).then(e=>{n||!t.current||(r=Ft.default.loadAnimation({container:t.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(()=>void 0),()=>{n=!0,r?.destroy()}},[e]),(0,H.jsx)(`div`,{className:`gift-animation-shell`,children:(0,H.jsx)(`div`,{className:`gift-animation`,ref:t})})}function hn(){let{t:e}=U(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(`official`),[p,m]=(0,g.useState)([]),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(`all`),[S,C]=(0,g.useState)(``),[w,T]=(0,g.useState)(!0),[E,D]=(0,g.useState)(`0`),[O,j]=(0,g.useState)(`0`),[M,N]=(0,g.useState)(``),[P,F]=(0,g.useState)(`0`),[I,L]=(0,g.useState)(``),[ee,R]=(0,g.useState)(`50`),[te,ne]=(0,g.useState)(`50`),[re,ae]=(0,g.useState)(`0`),[se,ce]=(0,g.useState)(!0),[le,ue]=(0,g.useState)(``),[z,de]=(0,g.useState)(null),[B,fe]=(0,g.useState)(!1),[pe,me]=(0,g.useState)(``),[he,ye]=(0,g.useState)(``);async function be(){me(``);try{n((await x.gifts()).Gifts??[])}catch(e){me(b(e))}}(0,g.useEffect)(()=>{be()},[]),(0,g.useEffect)(()=>{!a||d!==`official`||p.length>0||x.officialGifts().then(e=>m(e.gifts??[])).catch(e=>ye(b(e)))},[a,d,p.length]);let V=(0,g.useMemo)(()=>p.find(e=>e.source_gift_id===S)??null,[p,S]),xe=(0,g.useMemo)(()=>({all:p.length,upgrade:p.filter(e=>e.can_upgrade).length,craft:p.filter(e=>e.can_craft).length,basic:p.filter(e=>!e.can_upgrade).length}),[p]),Ce=(0,g.useMemo)(()=>{let e=h.trim().toLowerCase();return p.filter(t=>(v===`all`||v===`upgrade`&&t.can_upgrade||v===`craft`&&t.can_craft||v===`basic`&&!t.can_upgrade)&&(!e||t.source_gift_id.includes(e)||t.title.toLowerCase().includes(e)))},[p,h,v]),we=(0,g.useMemo)(()=>{let e=r.trim().toLowerCase();return e?t.filter(t=>String(t.GiftID).includes(e)||t.Title.toLowerCase().includes(e)||t.SourceFormat.toLowerCase().includes(e)):t},[t,r]);function Te(t,n=``){if(!l)throw Error(e(`gifts.fileRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));let r=new FormData;return r.set(`metadata`,JSON.stringify({command_id:n,reason:le.trim(),confirm:t,gift_id:P,title:I.trim(),stars:ee,convert_stars:te,enabled:se,sort_order:Number(re)})),r.set(`file`,l,l.name),r}function Ee(t,n=``){if(!S)throw Error(e(`gifts.officialRequired`));if(!le.trim())throw Error(e(`action.reasonRequired`));return{command_id:n,reason:le.trim(),confirm:t,source_gift_id:S,gift_id:P,title:I.trim(),stars:ee,convert_stars:te,enabled:se,sort_order:Number(re),include_collectible:w,upgrade_stars:E,supply_total:Number(O),slug_prefix:M.trim().toLowerCase()}}function De(t){C(t.source_gift_id),L(t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})),R(String(t.stars)),ne(String(t.convert_stars)),T(t.can_upgrade),D(t.upgrade_stars),j(String(t.availability_total||1)),N(`official-${t.source_gift_id}`),de(null)}async function ke(){fe(!0),ye(``),de(null);try{de(d===`official`?await x.importOfficialGift(Ee(!1)):await x.importGift(Te(!1)))}catch(e){ye(b(e))}finally{fe(!1)}}async function Ae(){if(z){fe(!0),ye(``);try{d===`official`?await x.importOfficialGift(Ee(!0,z.command_id)):await x.importGift(Te(!0,z.command_id)),de(null),u(null),F(`0`),L(``),C(``),await be(),o(!1)}catch(e){ye(b(e))}finally{fe(!1)}}}function Me(){F(`0`),L(``),R(`50`),ne(`50`),ae(`0`),ce(!0),ue(``),u(null),de(null),ye(``),f(`official`),C(``),_(``),y(`all`),o(!0)}function Ne(e){F(e.GiftID),L(e.Title),R(String(e.Stars)),ne(String(e.ConvertStars)),ae(String(e.SortOrder)),ce(e.Enabled),ue(``),u(null),de(null),ye(``),f(`official`),C(``),_(``),y(`all`),o(!0)}return(0,H.jsxs)(st,{title:e(`gifts.pageTitle`),eyebrow:e(`gifts.eyebrow`),actions:(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>be(),disabled:B,children:[(0,H.jsx)(_e,{size:15}),` `,e(`common.refresh`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:Me,children:[(0,H.jsx)(ge,{size:15}),` `,e(`gifts.add`)]})]}),children:[pe&&(0,H.jsx)(ut,{children:pe}),(0,H.jsxs)(`div`,{className:`metric-row gift-metrics`,children:[(0,H.jsx)(J,{label:e(`gifts.total`),value:String(t.length)}),(0,H.jsx)(J,{label:e(`gifts.enabled`),value:String(t.filter(e=>e.Enabled).length),tone:`good`}),(0,H.jsx)(J,{label:e(`gifts.received`),value:t.reduce((e,t)=>e+BigInt(t.ReceivedCount),0n).toString()}),(0,H.jsx)(J,{label:e(`gifts.formats`),value:`TGS / Lottie`})]}),(0,H.jsx)(ct,{children:(0,H.jsxs)(`div`,{className:`toolbar`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(ve,{size:15}),(0,H.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`gifts.searchPlaceholder`)})]}),(0,H.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:we.length,total:t.length})})]})}),(0,H.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table gift-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:e(`gifts.animation`)}),(0,H.jsx)(`th`,{children:e(`gifts.idRevision`)}),(0,H.jsx)(`th`,{children:e(`gifts.title`)}),(0,H.jsx)(`th`,{children:e(`gifts.price`)}),(0,H.jsx)(`th`,{children:e(`gifts.source`)}),(0,H.jsx)(`th`,{children:e(`gifts.received`)}),(0,H.jsx)(`th`,{children:e(`common.status`)}),(0,H.jsx)(`th`,{children:e(`common.updatedAt`)}),(0,H.jsx)(`th`,{children:e(`common.actions`)})]})}),(0,H.jsxs)(`tbody`,{children:[we.map(t=>(0,H.jsxs)(`tr`,{className:t.Enabled?``:`gift-row-disabled`,children:[(0,H.jsx)(`td`,{children:(0,H.jsx)(pn,{giftID:t.GiftID,revision:t.Revision,compact:!0})}),(0,H.jsxs)(`td`,{className:`mono`,children:[t.GiftID,` / `,t.Revision]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`strong`,{className:`gift-table-title`,children:t.Title||`Gift #${t.GiftID}`}),(0,H.jsxs)(`span`,{className:`gift-sort-order`,children:[e(`gifts.sortOrder`),`: `,t.SortOrder]})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsxs)(`strong`,{className:`gift-table-price`,children:[`⭐ `,t.Stars]}),(0,H.jsxs)(`span`,{className:`gift-convert-price`,children:[`→ `,t.ConvertStars]})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(q,{children:t.SourceFormat}),(0,H.jsx)(`span`,{className:`gift-source-size`,children:fn(t.AnimationSize)})]}),(0,H.jsx)(`td`,{children:t.ReceivedCount}),(0,H.jsx)(`td`,{children:(0,H.jsx)(q,{tone:t.Enabled?`good`:`neutral`,children:t.Enabled?e(`common.enabled`):e(`common.disabled`)})}),(0,H.jsx)(`td`,{children:rt(t.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,H.jsxs)(`button`,{className:`btn compact-btn collectible-button`,type:`button`,onClick:()=>c(t),children:[(0,H.jsx)(oe,{size:13}),e(`collectibles.manage`)]}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>Ne(t),children:e(`gifts.replace`)}),(0,H.jsx)(vt,{compact:!0,tone:`neutral`,label:t.Enabled?e(`gifts.disable`):e(`gifts.enable`),path:`/api/actions/set-gift-enabled`,payload:()=>({gift_id:t.GiftID,enabled:!t.Enabled}),onDone:()=>void be()})]})})]},t.GiftID)),we.length===0&&(0,H.jsx)(pt,{colSpan:9})]})]})}),a&&(0,_t.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal gift-import-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P}),children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:e(`gifts.importEyebrow`)}),(0,H.jsx)(`h2`,{children:P===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:P})})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>o(!1),disabled:B,"aria-label":e(`action.close`),children:(0,H.jsx)(je,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body gift-import-modal-body`,children:[(0,H.jsxs)(`div`,{className:`command-steps`,children:[(0,H.jsxs)(`div`,{className:`command-step ${(d===`official`?S:l)?`done`:`active`}`,children:[(0,H.jsx)(`span`,{children:`1`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepDetails`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${z?`done`:(d===`official`?S:l)?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`2`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepValidate`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${z?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`3`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepImport`)})]})]}),(0,H.jsxs)(`div`,{className:`gift-source-tabs`,children:[(0,H.jsx)(`button`,{className:`btn ${d===`official`?`primary`:``}`,type:`button`,onClick:()=>{f(`official`),de(null)},children:e(`gifts.officialSource`)}),(0,H.jsx)(`button`,{className:`btn ${d===`file`?`primary`:``}`,type:`button`,onClick:()=>{f(`file`),de(null)},children:e(`gifts.fileSource`)})]}),d===`official`?(0,H.jsxs)(`section`,{className:`official-gift-picker`,children:[(0,H.jsxs)(`div`,{className:`gift-import-note`,children:[(0,H.jsx)(`span`,{children:e(`gifts.officialHint`)}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,H.jsx)(`span`,{children:p.length}),(0,H.jsx)(`span`,{children:`SHA-256`})]})]}),(0,H.jsxs)(`div`,{className:`official-gift-tools`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(ve,{size:15}),(0,H.jsx)(`input`,{value:h,onChange:e=>_(e.target.value),placeholder:e(`gifts.officialSearch`)})]}),(0,H.jsx)(`span`,{children:e(`gifts.officialResults`,{shown:Ce.length,total:p.length})})]}),(0,H.jsx)(`div`,{className:`official-gift-categories`,role:`group`,"aria-label":e(`gifts.officialCategoryLabel`),children:[`all`,`upgrade`,`craft`,`basic`].map(t=>(0,H.jsxs)(`button`,{className:v===t?`active`:``,type:`button`,"aria-pressed":v===t,onClick:()=>y(t),children:[e(`gifts.officialCategory.${t}`),(0,H.jsx)(`span`,{children:xe[t]})]},t))}),(0,H.jsxs)(`div`,{className:`official-gift-list`,role:`listbox`,"aria-label":e(`gifts.officialSelect`),children:[Ce.map(t=>{let n=t.source_gift_id===S;return(0,H.jsxs)(`button`,{className:`official-gift-option ${n?`selected`:``}`,type:`button`,role:`option`,"aria-selected":n,onClick:()=>De(t),children:[(0,H.jsxs)(`span`,{className:`official-gift-option-head`,children:[(0,H.jsx)(`strong`,{children:t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})}),(0,H.jsxs)(`span`,{className:`mono`,children:[`#`,t.source_gift_id]})]}),(0,H.jsxs)(`span`,{className:`official-gift-option-meta`,children:[(0,H.jsxs)(`span`,{children:[`⭐ `,t.stars]}),(0,H.jsx)(`span`,{children:e(`gifts.officialAttributes`,{count:dn(t)})})]}),(0,H.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,H.jsx)(`span`,{className:t.can_upgrade?`yes`:`no`,children:t.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,H.jsx)(`span`,{className:t.can_craft?`craft`:`no`,children:t.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]},t.source_gift_id)}),Ce.length===0&&(0,H.jsx)(`div`,{className:`official-gift-empty`,children:e(`gifts.officialEmpty`)})]}),V&&(0,H.jsxs)(`div`,{className:`official-gift-selected`,children:[(0,H.jsx)(mn,{sourceGiftID:V.source_gift_id}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:V.title||e(`gifts.officialUnnamed`,{id:V.source_gift_id})}),(0,H.jsx)(`span`,{className:`mono`,children:V.source_gift_id}),(0,H.jsxs)(`small`,{children:[V.model_count,` `,e(`collectibles.models`),` · `,V.pattern_count,` `,e(`collectibles.patterns`),` · `,V.backdrop_count,` `,e(`collectibles.backdrops`)]}),(0,H.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,H.jsx)(`span`,{className:V.can_upgrade?`yes`:`no`,children:V.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,H.jsx)(`span`,{className:V.can_craft?`craft`:`no`,children:V.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]})]}),V?.can_upgrade&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>{T(e.target.checked),de(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:e(`gifts.includeCollectible`)})]}),w&&(0,H.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.upgradeStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:E,onChange:e=>{D(e.target.value),de(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.supply`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:O,onChange:e=>{j(e.target.value),de(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.slug`)}),(0,H.jsx)(`input`,{value:M,maxLength:48,onChange:e=>{N(e.target.value.toLowerCase()),de(null)}})]})]})]})]}):(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`div`,{className:`gift-import-note`,children:[(0,H.jsx)(`span`,{children:e(`gifts.importHint`)}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,"aria-label":e(`gifts.formats`),children:[(0,H.jsx)(`span`,{children:`TGS`}),(0,H.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,H.jsxs)(`label`,{className:`gift-file-picker ${l?`has-file`:``}`,children:[(0,H.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:e=>{u(e.target.files?.[0]??null),de(null)}}),(0,H.jsx)(`span`,{className:`gift-file-icon`,children:(0,H.jsx)(ie,{size:22})}),(0,H.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,H.jsx)(`span`,{className:`gift-field-label`,children:e(`gifts.animation`)}),(0,H.jsx)(`strong`,{children:l?l.name:e(`gifts.filePrompt`)}),(0,H.jsx)(`small`,{children:l?fn(l.size):e(`gifts.fileHint`)})]}),(0,H.jsx)(`span`,{className:`gift-file-action`,children:e(l?`gifts.changeFile`:`gifts.chooseFile`)})]})]}),(0,H.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.title`)}),(0,H.jsx)(`input`,{value:I,maxLength:128,placeholder:e(`gifts.titlePlaceholder`),onChange:e=>{L(e.target.value),de(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.stars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:ee,onChange:e=>{R(e.target.value),de(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.convertStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:te,onChange:e=>{ne(e.target.value),de(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:re,onChange:e=>{ae(e.target.value),de(null)}})]})]}),(0,H.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,H.jsx)(`span`,{children:e(`gifts.reason`)}),(0,H.jsx)(`input`,{value:le,placeholder:e(`gifts.reasonPlaceholder`),onChange:e=>ue(e.target.value)})]}),(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:se,onChange:e=>{ce(e.target.checked),de(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:e(`gifts.enableAfterImport`)})]}),he&&(0,H.jsx)(ut,{children:he}),z&&(0,H.jsxs)(`div`,{className:`gift-validation`,children:[(0,H.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,H.jsx)(k,{size:17}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e(`gifts.validationReady`)}),(0,H.jsx)(`span`,{children:e(`gifts.validationHint`)})]})]}),(0,H.jsx)(`pre`,{children:JSON.stringify(z.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>o(!1),disabled:B,children:e(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:ke,disabled:B,children:[B?(0,H.jsx)(A,{className:`spin`,size:15}):(0,H.jsx)(Se,{size:15}),e(`gifts.validate`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:Ae,disabled:B||!z,children:[(0,H.jsx)(Oe,{size:15}),e(`gifts.confirmImport`)]})]})]})}),document.body),s&&(0,H.jsx)(un,{gift:s,onClose:()=>c(null),onPublished:()=>void be()})]})}var gn=`777000`;function _n(e){let t=e.rarity_permille>0?` · ${(e.rarity_permille/10).toFixed(1)}%`:``;return`${e.name||`#${e.id}`}${t}`}function vn({gift:e,onDone:t}){let{t:n}=U(),[r,i]=(0,g.useState)(`user`),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(``),[d,f]=(0,g.useState)(!1),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(null),[v,y]=(0,g.useState)(``),[S,C]=(0,g.useState)(`0`),[w,T]=(0,g.useState)(`0`),[E,D]=(0,g.useState)(`0`),[j,M]=(0,g.useState)(``),[N,P]=(0,g.useState)(null),[F,I]=(0,g.useState)(``),[L,ee]=(0,g.useState)(!1),R=r===`user`?a?.ID??0:s?.ID??0,te=r===`user`&&p;(0,g.useEffect)(()=>{m(!1),_(null),y(``),C(`0`),T(`0`),D(`0`),P(null),I(``)},[e.GiftID]),(0,g.useEffect)(()=>{if(!te||h)return;let t=!1;return y(``),x.giftCollectibles(e.GiftID).then(e=>{t||_(e)}).catch(e=>{t||y(b(e))}),()=>{t=!0}},[te,h,e.GiftID]);function ne(t){return{gift_id:e.GiftID,sender_user_id:Number(gn),user_id:r===`user`?R:0,channel_id:r===`channel`?R:0,hide_name:d,message:l.trim(),upgrade:te,model_attribute_id:te?S:`0`,pattern_attribute_id:te?w:`0`,backdrop_attribute_id:te?E:`0`,reason:j.trim(),confirm:t}}let re=(0,g.useMemo)(()=>ne(!1),[e.GiftID,r,R,l,d,p,S,w,E,j]),ie=N?.dry_run&&!N.error;async function ae(e){if(R<=0){I(n(`giveGift.recipientRequired`));return}if(!j.trim()){I(n(`action.reasonRequired`));return}ee(!0),I(``);try{let n=await x.action(`/api/actions/give-gift`,ne(e));P(n),e&&!n.error&&t?.()}catch(e){I(b(e))}finally{ee(!1)}}return(0,H.jsxs)(`div`,{className:`give-gift-form`,children:[(0,H.jsxs)(`div`,{className:`give-gift-summary`,children:[(0,H.jsx)(se,{size:16}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e.Title||`Gift #${e.GiftID}`}),(0,H.jsxs)(`span`,{className:`mono`,children:[`#`,e.GiftID,` · ⭐ `,e.Stars]})]})]}),(0,H.jsxs)(`div`,{className:`give-gift-tabs`,role:`group`,"aria-label":n(`giveGift.recipientKind`),children:[(0,H.jsxs)(`button`,{type:`button`,className:`btn ${r===`user`?`primary`:``}`,onClick:()=>{i(`user`),P(null)},children:[(0,H.jsx)(ke,{size:15}),` `,n(`giveGift.recipientUser`)]}),(0,H.jsxs)(`button`,{type:`button`,className:`btn ${r===`channel`?`primary`:``}`,onClick:()=>{i(`channel`),m(!1),P(null)},children:[(0,H.jsx)(Ae,{size:15}),` `,n(`giveGift.recipientChannel`)]})]}),r===`user`?(0,H.jsx)(Gt,{label:n(`giveGift.pickUser`),value:a,onChange:e=>{o(e),P(null)}}):(0,H.jsx)(Kt,{label:n(`giveGift.pickChannel`),value:s,onChange:e=>{c(e),P(null)}}),(0,H.jsxs)(`label`,{className:`form-field`,children:[(0,H.jsx)(`span`,{children:n(`giveGift.sender`)}),(0,H.jsx)(`input`,{value:gn,disabled:!0,readOnly:!0}),(0,H.jsx)(`small`,{className:`field-hint`,children:n(`giveGift.senderHint`)})]}),(0,H.jsxs)(`label`,{className:`form-field`,children:[(0,H.jsx)(`span`,{children:n(`giveGift.message`)}),(0,H.jsx)(`textarea`,{value:l,rows:2,maxLength:128,onChange:e=>{u(e.target.value),P(null)},placeholder:n(`giveGift.messagePlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:d,onChange:e=>{f(e.target.checked),P(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:n(`giveGift.hideName`)})]}),r===`user`&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>{m(e.target.checked),e.target.checked||(C(`0`),T(`0`),D(`0`)),P(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:n(`giveGift.upgrade`)})]}),p&&(0,H.jsx)(`p`,{className:`give-gift-upgrade-note`,children:n(`giveGift.upgradeNote`)}),p&&v&&(0,H.jsx)(ut,{children:v}),p&&h&&(0,H.jsxs)(`div`,{className:`gift-fields-grid give-gift-attrs`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:n(`giveGift.model`)}),(0,H.jsxs)(`select`,{value:S,onChange:e=>{C(e.target.value),P(null)},children:[(0,H.jsx)(`option`,{value:`0`,children:n(`giveGift.random`)}),(h.models??[]).map(e=>(0,H.jsx)(`option`,{value:e.id,children:_n(e)},e.id))]})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:n(`giveGift.pattern`)}),(0,H.jsxs)(`select`,{value:w,onChange:e=>{T(e.target.value),P(null)},children:[(0,H.jsx)(`option`,{value:`0`,children:n(`giveGift.random`)}),(h.patterns??[]).map(e=>(0,H.jsx)(`option`,{value:e.id,children:_n(e)},e.id))]})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:n(`giveGift.backdrop`)}),(0,H.jsxs)(`select`,{value:E,onChange:e=>{D(e.target.value),P(null)},children:[(0,H.jsx)(`option`,{value:`0`,children:n(`giveGift.random`)}),(h.backdrops??[]).map(e=>(0,H.jsx)(`option`,{value:e.id,children:_n(e)},e.id))]})]})]})]}),(0,H.jsxs)(`label`,{className:`form-field`,children:[(0,H.jsx)(`span`,{children:n(`action.reason`)}),(0,H.jsx)(`textarea`,{value:j,rows:2,onChange:e=>M(e.target.value),placeholder:n(`action.reasonPlaceholder`)})]}),(0,H.jsxs)(`div`,{className:`command-preview`,children:[(0,H.jsx)(`div`,{className:`preview-head`,children:n(`action.requestPreview`)}),(0,H.jsx)(ht,{value:JSON.stringify(re,null,2)})]}),F&&(0,H.jsx)(ut,{children:F}),N&&(0,H.jsxs)(`div`,{className:`result-box`,children:[(0,H.jsxs)(`div`,{className:`result-title`,children:[N.error?(0,H.jsx)(O,{size:16}):(0,H.jsx)(k,{size:16}),(0,H.jsx)(`strong`,{children:N.message||N.error||n(`action.result`)})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:n(`action.commandID`)}),(0,H.jsx)(`strong`,{children:N.command_id})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:n(`action.status`)}),(0,H.jsx)(`strong`,{children:N.status})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:n(`action.dryRun`)}),(0,H.jsx)(`strong`,{children:N.dry_run?n(`common.yes`):n(`common.no`)})]}),N.details&&(0,H.jsx)(ht,{value:JSON.stringify(N.details,null,2)})]}),(0,H.jsxs)(`div`,{className:`give-gift-form-actions`,children:[(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>ae(!1),disabled:L,children:[L?(0,H.jsx)(A,{size:15,className:`spin`}):(0,H.jsx)(he,{size:15}),n(N?`action.runAgain`:`action.runDry`)]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>ae(!0),disabled:L||!ie,children:[(0,H.jsx)(se,{size:15}),n(`giveGift.confirm`)]})]})]})}function yn(){let{t:e}=U(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(!1);async function d(){u(!0),c(``);try{let e=(await x.gifts()).Gifts??[];n(e),o(t=>t??e[0]??null)}catch(e){c(b(e))}finally{u(!1)}}(0,g.useEffect)(()=>{d()},[]);let f=(0,g.useMemo)(()=>{let e=r.trim().toLowerCase();return e?t.filter(t=>String(t.GiftID).includes(e)||t.Title.toLowerCase().includes(e)):t},[t,r]);return(0,H.jsxs)(st,{title:e(`giveGifts.pageTitle`),eyebrow:e(`giveGifts.eyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>d(),disabled:l,children:[(0,H.jsx)(_e,{size:15}),` `,e(`common.refresh`)]}),children:[s&&(0,H.jsx)(ut,{children:s}),(0,H.jsx)(`p`,{className:`give-gift-upgrade-note`,children:e(`giveGifts.hint`)}),(0,H.jsxs)(`div`,{className:`give-gift-layout`,children:[(0,H.jsxs)(`section`,{className:`give-gift-picker`,children:[(0,H.jsxs)(`div`,{className:`give-gift-picker-head`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(ve,{size:15}),(0,H.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`giveGifts.searchPlaceholder`)})]}),(0,H.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:f.length,total:t.length})})]}),(0,H.jsxs)(`div`,{className:`give-gift-picker-list`,role:`listbox`,"aria-label":e(`giveGifts.pickGift`),children:[f.map(t=>{let n=a?.GiftID===t.GiftID;return(0,H.jsxs)(`button`,{type:`button`,role:`option`,"aria-selected":n,className:`give-gift-option ${n?`selected`:``} ${t.Enabled?``:`gift-row-disabled`}`,onClick:()=>o(t),children:[(0,H.jsx)(It,{className:`give-gift-thumb`,cacheKey:`${t.GiftID}:${t.Revision}`,loader:()=>x.giftAnimation(t.GiftID)}),(0,H.jsxs)(`span`,{className:`give-gift-option-info`,children:[(0,H.jsx)(`strong`,{children:t.Title||`Gift #${t.GiftID}`}),(0,H.jsxs)(`span`,{className:`mono`,children:[`#`,t.GiftID]})]}),(0,H.jsx)(`span`,{className:`give-gift-option-price`,children:t.Enabled?(0,H.jsxs)(q,{children:[`⭐ `,t.Stars]}):(0,H.jsx)(q,{tone:`neutral`,children:e(`common.disabled`)})})]},t.GiftID)}),f.length===0&&!l&&(0,H.jsx)(`div`,{className:`official-gift-empty`,children:e(`common.noResults`)})]})]}),(0,H.jsx)(`section`,{className:`give-gift-panel`,children:a?(0,H.jsx)(vn,{gift:a,onDone:()=>void d()},a.GiftID):(0,H.jsxs)(`div`,{className:`give-gift-empty-panel`,children:[(0,H.jsx)(se,{size:26}),(0,H.jsx)(`p`,{children:e(`giveGifts.selectPrompt`)})]})})]})]})}var bn=`open,in_review,action_pending,action_failed,appeal_review`,xn=[{value:bn,labelKey:`moderation.statusFilter.active`},{value:`open,in_review,action_pending,action_failed,resolved,dismissed,appeal_review`,labelKey:`moderation.statusFilter.all`},{value:`open`,labelKey:`moderation.status.open`},{value:`in_review`,labelKey:`moderation.status.in_review`},{value:`action_pending`,labelKey:`moderation.status.action_pending`},{value:`action_failed`,labelKey:`moderation.status.action_failed`},{value:`appeal_review`,labelKey:`moderation.status.appeal_review`},{value:`resolved`,labelKey:`moderation.status.resolved`},{value:`dismissed`,labelKey:`moderation.status.dismissed`}];function Sn({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(bn),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);try{let e=new URLSearchParams({statuses:n,limit:`100`});i.trim()&&e.set(`assigned_to`,i.trim()),s((await x.moderationCases(e)).cases)}catch(e){d(b(e))}finally{l(!1)}}(0,g.useEffect)(()=>{f()},[]);let p=o.filter(e=>e.Status===`action_pending`||e.Status===`action_failed`).length,m=o.filter(e=>e.Severity===4).length;return(0,H.jsxs)(st,{title:t(`route.moderation`),eyebrow:t(`moderation.casesEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:f,disabled:c,children:[(0,H.jsx)(_e,{size:15,className:c?`spin`:``}),` `,t(`common.refresh`)]}),children:[u&&(0,H.jsx)(ut,{children:u}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(J,{label:t(`moderation.currentQueue`),value:String(o.length)}),(0,H.jsx)(J,{label:t(`moderation.criticalCases`),value:String(m),tone:m?`danger`:`neutral`}),(0,H.jsx)(J,{label:t(`moderation.pendingOrFailed`),value:String(p),tone:p?`warn`:`good`})]}),(0,H.jsx)(ct,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),f()},children:[(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.status`)}),(0,H.jsx)(`select`,{"aria-label":t(`moderation.statusFilter`),value:n,onChange:e=>r(e.target.value),children:xn.map(e=>(0,H.jsx)(`option`,{value:e.value,children:t(e.labelKey)},e.value))})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`moderation.assignee`)}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`moderation.allAssignees`)})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:c,children:[(0,H.jsx)(xe,{size:15}),` `,t(`common.search`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`moderation.case`)}),(0,H.jsx)(`th`,{children:t(`moderation.target`)}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`moderation.severity`)}),(0,H.jsx)(`th`,{children:t(`moderation.reportsAndReporters`)}),(0,H.jsx)(`th`,{children:t(`moderation.assignee`)}),(0,H.jsx)(`th`,{children:t(`moderation.latestReport`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsxs)(`td`,{className:`mono`,children:[`#`,n.ID]}),(0,H.jsx)(`td`,{className:`mono`,children:En(t,n.Target.Type,n.Target.ID)}),(0,H.jsx)(`td`,{children:(0,H.jsx)(Cn,{status:n.Status})}),(0,H.jsx)(`td`,{children:(0,H.jsx)(wn,{value:n.Severity})}),(0,H.jsxs)(`td`,{children:[n.ReportCount,` / `,n.DistinctReporterCount]}),(0,H.jsx)(`td`,{children:n.AssignedTo||`-`}),(0,H.jsx)(`td`,{children:rt(n.LastReportAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/moderation/${n.ID}`),children:[t(`moderation.review`),` `,(0,H.jsx)(R,{size:14})]})})]},n.ID)),o.length===0&&(0,H.jsx)(pt,{colSpan:8})]})]})})]})}function Cn({status:e}){let{t}=U();return(0,H.jsx)(q,{tone:e===`resolved`||e===`dismissed`?`good`:e===`action_failed`?`danger`:e===`action_pending`?`warn`:`neutral`,children:Tn(t,`status`,e)})}function wn({value:e}){let{t}=U(),n=[``,`low`,`medium`,`high`,`critical`][e];return(0,H.jsx)(q,{tone:e>=4?`danger`:e>=3?`warn`:`neutral`,children:n?t(`moderation.severity.${n}`):e})}function Tn(e,t,n){let r=`moderation.${t}.${n}`,i=e(r);return i===r?n:i}function En(e,t,n){return`${Tn(e,`targetType`,t)} #${n}`}function Dn({id:e,navigate:t}){let{t:n}=U(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(`no_violation`),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(``),[h,_]=(0,g.useState)(!0),[v,y]=(0,g.useState)(!1),[S,C]=(0,g.useState)(``);function w(e){o(e),e&&(f(e.Items.filter(e=>e.Kind===`message`).map(e=>Number(e.ItemID)).filter(e=>Number.isSafeInteger(e)&&e>0).join(`, `)),m(String(e.ReporterUserID)))}async function T(){C(``);try{let t=await x.moderationCase(e);i(t);let n=t.ReportIDs[0];w(n?await x.moderationReport(n):null)}catch(e){C(b(e))}}(0,g.useEffect)(()=>{T()},[e]);let E=(0,g.useMemo)(()=>On(l,r?.Case.Target.Type,kn(d),Number(p),h),[l,r?.Case.Target.Type,d,p,h]),D=(0,g.useMemo)(()=>r?An(r,n):{actions:[],label:n(`common.none`),blocked:!1},[r,n]);async function O(){if(r){y(!0),C(``);try{await x.claimModerationCase(e,r.Case.Version),await T()}catch(e){C(b(e))}finally{y(!1)}}}async function A(){if(!r||!s.trim()){C(n(`moderation.reasonRequired`));return}if(l===`delete_messages`&&E.length===0){C(r.Case.Target.Type===`user`?n(`moderation.privateDeleteValidation`):n(`moderation.channelDeleteValidation`));return}if(window.confirm(n(`moderation.confirmDecision`,{decision:jn(n,l)}))){y(!0),C(``);try{i((await x.decideModerationCase(e,{expected_version:r.Case.Version,reason:s.trim(),kind:l===`no_violation`?`no_violation`:`violation`,actions:E})).case),c(``)}catch(e){C(b(e))}finally{y(!1)}}}async function j(t,a){if(!r||!s.trim()){C(n(`moderation.appealReasonRequired`));return}if(window.confirm(n(a?`moderation.confirmGrantAppeal`:`moderation.confirmDenyAppeal`))){y(!0);try{i((await x.reviewModerationAppeal(e,t,{expected_version:r.Case.Version,reason:s.trim(),granted:a,actions:a?D.actions:[]})).case),c(``)}catch(e){C(b(e))}finally{y(!1)}}}if(S&&!r)return(0,H.jsx)(ut,{children:S});if(!r)return(0,H.jsx)(mt,{label:n(`moderation.loadingCase`)});let M=r.Case,P=M.Status===`open`||M.Status===`in_review`||M.Status===`appeal_review`,F=(M.Status===`in_review`||M.Status===`action_failed`)&&!!M.AssignedTo,I=F&&(M.Status!==`action_failed`||l!==`no_violation`),L=r.Appeals.find(e=>e.Status===`pending`);return(0,H.jsxs)(st,{title:n(`moderation.caseDetailTitle`,{id:M.ID}),eyebrow:n(`moderation.caseDetailEyebrow`),actions:(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/moderation`),children:[(0,H.jsx)(N,{size:15}),` `,n(`moderation.backToQueue`)]}),(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:T,children:[(0,H.jsx)(_e,{size:15}),` `,n(`common.refresh`)]})]}),children:[S&&(0,H.jsx)(ut,{children:S}),(0,H.jsx)(lt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:En(n,M.Target.Type,M.Target.ID)}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:n(`moderation.versionAndUpdated`,{version:M.Version,time:rt(M.UpdatedAt)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(Cn,{status:M.Status}),(0,H.jsx)(wn,{value:M.Severity})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Y,{label:n(`moderation.target`),value:En(n,M.Target.Type,M.Target.ID),mono:!0}),(0,H.jsx)(Y,{label:n(`moderation.reportCount`),value:n(`moderation.reportCountValue`,{reports:M.ReportCount,reporters:M.DistinctReporterCount})}),(0,H.jsx)(Y,{label:n(`moderation.assignee`),value:M.AssignedTo||`-`}),(0,H.jsx)(Y,{label:n(`moderation.firstAndLatestReport`),value:`${rt(M.FirstReportAt)} / ${rt(M.LastReportAt)}`})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:n(`moderation.evidence`),text:n(`moderation.evidenceHint`)}),(0,H.jsx)(`div`,{className:`toolbar`,children:r.ReportIDs.map(e=>(0,H.jsxs)(`button`,{className:`btn`,onClick:async()=>w(await x.moderationReport(e)),children:[`#`,e]},e))}),a&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Y,{label:n(`moderation.sourceAndReason`),value:`${Tn(n,`source`,a.Source)} / ${Tn(n,`reason`,a.Reason)}`}),(0,H.jsx)(Y,{label:n(`moderation.reporter`),value:String(a.ReporterUserID),mono:!0}),(0,H.jsx)(Y,{label:n(`moderation.option`),value:a.Option,mono:!0}),(0,H.jsx)(Y,{label:n(`common.time`),value:rt(a.CreatedAt)})]}),a.Comment&&(0,H.jsx)(`p`,{className:`about-text`,children:a.Comment}),(0,H.jsx)(ht,{value:JSON.stringify(a,null,2)})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:n(`moderation.decisionAudit`),text:n(`moderation.decisionAuditHint`)}),(0,H.jsx)(ht,{value:JSON.stringify({decisions:r.Decisions,actions:r.Actions},null,2)})]}),r.Appeals.length>0&&(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(K,{title:n(`moderation.appeals`)}),(0,H.jsx)(ht,{value:JSON.stringify(r.Appeals,null,2)})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`moderation.caseActions`)}),P&&(0,H.jsxs)(`button`,{className:`btn primary icon-text`,disabled:v,onClick:O,children:[(0,H.jsx)(Se,{size:15}),` `,M.AssignedTo?n(`moderation.renewClaim`):n(`moderation.claimCase`)]}),(0,H.jsxs)(`label`,{className:`field`,children:[(0,H.jsx)(`span`,{children:n(`moderation.reviewReason`)}),(0,H.jsx)(`textarea`,{value:s,onChange:e=>c(e.target.value),rows:5})]}),(0,H.jsxs)(`label`,{className:`field`,children:[(0,H.jsx)(`span`,{children:n(`moderation.decisionPreset`)}),(0,H.jsxs)(`select`,{value:l,onChange:e=>u(e.target.value),children:[(0,H.jsx)(`option`,{value:`no_violation`,children:n(`moderation.preset.noViolation`)}),(0,H.jsx)(`option`,{value:`scam`,children:n(`moderation.preset.scam`)}),(0,H.jsx)(`option`,{value:`fake`,children:n(`moderation.preset.fake`)}),(0,H.jsx)(`option`,{value:`freeze`,children:n(`moderation.preset.freeze`)}),(0,H.jsx)(`option`,{value:`scam_freeze`,children:n(`moderation.preset.scamFreeze`)}),(0,H.jsx)(`option`,{value:`fake_freeze`,children:n(`moderation.preset.fakeFreeze`)}),(0,H.jsx)(`option`,{value:`delete_messages`,children:n(`moderation.preset.deleteMessages`)}),(0,H.jsx)(`option`,{value:`delete_account`,children:n(`moderation.preset.deleteAccount`)})]})]}),l===`delete_messages`&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`field`,children:[(0,H.jsx)(`span`,{children:n(`moderation.evidenceMessageIDs`)}),(0,H.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),placeholder:`101, 102`})]}),M.Target.Type===`user`&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`field`,children:[(0,H.jsx)(`span`,{children:n(`moderation.privateOwnerUserID`)}),(0,H.jsx)(`input`,{value:p,onChange:e=>m(e.target.value),inputMode:`numeric`})]}),(0,H.jsxs)(`label`,{className:`field checkbox-field`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:h,onChange:e=>_(e.target.checked)}),(0,H.jsx)(`span`,{children:n(`moderation.revokeForBoth`)})]})]}),(0,H.jsx)(ut,{children:n(`moderation.evidenceValidationHint`)})]}),M.Status===`action_failed`&&l===`no_violation`&&(0,H.jsx)(ut,{children:n(`moderation.failedActionHint`)}),F&&(0,H.jsxs)(`button`,{className:`btn danger icon-text`,disabled:v||!I,onClick:A,children:[(0,H.jsx)(k,{size:15}),` `,M.Status===`action_failed`?n(`moderation.retryAction`):n(`moderation.submitDecision`)]}),L&&M.AssignedTo&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`moderation.appealReviewTitle`,{id:L.ID})}),(0,H.jsx)(Y,{label:n(`moderation.automaticRemedy`),value:D.label}),D.blocked&&(0,H.jsx)(ut,{children:n(`moderation.irreversibleAppealHint`)}),(0,H.jsx)(`button`,{className:`btn`,disabled:v,onClick:()=>j(L.ID,!1),children:n(`moderation.denyAppeal`)}),(0,H.jsx)(`button`,{className:`btn primary`,disabled:v||D.blocked,onClick:()=>j(L.ID,!0),children:n(`moderation.grantAppeal`)})]})]})})]})}function On(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 kn(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 An(e,t){let n=!1,r=!1,i=!1;for(let t of[...e.Actions].sort((e,t)=>e.ID-t.ID))if(t.Status===`succeeded`)switch(t.Kind){case`mark_scam`:case`mark_fake`:n=!0;break;case`clear_peer_flags`:n=!1;break;case`freeze_account`:r=!0;break;case`unfreeze_account`:r=!1;break;case`delete_private_message`:case`delete_channel_message`:case`delete_account`:i=!0;break}let a=[],o=[];return n&&(a.push({kind:`clear_peer_flags`,payload:{}}),o.push(t(`moderation.remedy.clearFlags`))),r&&(a.push({kind:`unfreeze_account`,payload:{}}),o.push(t(`moderation.remedy.unfreeze`))),{actions:a,label:o.join(` + `)||t(`moderation.remedy.none`),blocked:i}}function jn(e,t){return e(`moderation.preset.${{no_violation:`noViolation`,scam:`scam`,fake:`fake`,freeze:`freeze`,scam_freeze:`scamFreeze`,fake_freeze:`fakeFreeze`,delete_messages:`deleteMessages`,delete_account:`deleteAccount`}[t]}`)}function Mn({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];return n?(0,H.jsx)(Dt,{id:Number(n),navigate:t}):r?(0,H.jsx)(X,{id:Number(r),navigate:t}):i?(0,H.jsx)(Nt,{id:Number(i),navigate:t}):a?(0,H.jsx)(Dn,{id:Number(a),navigate:t}):e.path===`/accounts`?(0,H.jsx)(jt,{navigate:t}):e.path===`/channels`?(0,H.jsx)(Mt,{navigate:t}):e.path===`/bots`?(0,H.jsx)(Pt,{navigate:t}):e.path===`/moderation`?(0,H.jsx)(Sn,{navigate:t}):e.path===`/emoji`?(0,H.jsx)(Vt,{}):e.path===`/gifts`?(0,H.jsx)(hn,{}):e.path===`/give-gifts`?(0,H.jsx)(yn,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,H.jsx)(Jt,{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,H.jsx)(Wt,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups`?(0,H.jsx)(qt,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,H.jsx)(Yt,{navigate:t}):(0,H.jsx)(Ht,{navigate:t})}function Nn(){let[e,t]=(0,g.useState)(void 0),[n,r]=(0,g.useState)(()=>W());(0,g.useEffect)(()=>{let e=()=>r(W());return window.addEventListener(`popstate`,e),()=>window.removeEventListener(`popstate`,e)},[]),(0,g.useEffect)(()=>{x.session().then(e=>t(e.actor)).catch(e=>{if(e instanceof v&&e.status===401){t(null);return}t(null)})},[]);let i=e=>{window.history.pushState(null,``,e),r(W())};return e===void 0?(0,H.jsx)(Xe,{}):e===null?(0,H.jsx)(gt,{onLogin:t}):(0,H.jsx)(Ze,{actor:e,route:n,navigate:i,onLogout:()=>t(null),children:(0,H.jsx)(Mn,{route:n,navigate:i})})}_.createRoot(document.getElementById(`root`)).render((0,H.jsx)(g.StrictMode,{children:(0,H.jsx)(Ke,{children:(0,H.jsx)(Ie,{children:(0,H.jsx)(Nn,{})})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/assets/index-D_BLAfeq.js b/cmd/telesrv-admin/web/dist/assets/index-D_BLAfeq.js new file mode 100644 index 00000000..32fe20c4 --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-D_BLAfeq.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 z(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 pe(e,t){var n=t.checked;return L({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function me(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 he(e,t){t=t.checked,t!=null&&S(e,`checked`,t,!1)}function ge(e,t){he(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`)?ve(e,t.type,n):t.hasOwnProperty(`defaultValue`)&&ve(e,t.type,le(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function _e(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 ve(e,t,n){(t!==`number`||z(e.ownerDocument)!==e)&&(n==null?e.defaultValue=``+e._wrapperState.initialValue:e.defaultValue!==``+n&&(e.defaultValue=``+n))}var ye=Array.isArray;function be(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i`+t.valueOf().toString()+``,t=Ee.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Oe(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var ke={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},Ae=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(ke).forEach(function(e){Ae.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),ke[t]=ke[e]})});function je(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||ke.hasOwnProperty(e)&&ke[e]?(``+t).trim():t+`px`}function Me(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=je(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var Ne=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 Pe(e,t){if(t){if(Ne[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 Fe(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 Ie=null;function Le(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Re=null,ze=null,Be=null;function Ve(e){if(e=Ai(e)){if(typeof Re!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Mi(t),Re(e.stateNode,e.type,t))}}function He(e){ze?Be?Be.push(e):Be=[e]:ze=e}function Ue(){if(ze){var e=ze,t=Be;if(Be=ze=null,Ve(e),t)for(e=0;e>>=0,e===0?32:31-(xt(e)/St|0)|0}var wt=64,Tt=4194304;function Et(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 Dt(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=Et(a))):r=Et(s)}else o=n&~i,o===0?a!==0&&(r=Et(a)):r=Et(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-bt(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=Un),Kn=` `,qn=!1;function Jn(e,t){switch(e){case`keyup`:return Vn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function Yn(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var Xn=!1;function Zn(e,t){switch(e){case`compositionend`:return Yn(t);case`keypress`:return t.which===32?(qn=!0,Kn):null;case`textInput`:return e=t.data,e===Kn&&qn?null:e;default:return null}}function Qn(e,t){if(Xn)return e===`compositionend`||!Hn&&Jn(e,t)?(e=fn(),dn=un=ln=null,Xn=!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=z();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=z(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!==z(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=Z;try{var n=Yi;for(Z=1;e>=o,i-=o,ca=1<<32-bt(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(ye(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,X(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{Z=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,vr(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,X(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-bt(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=Fe(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*ft()-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=ft(),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=nn,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},nn=!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(vt&&typeof vt.onCommitFiberUnmount==`function`)try{vt.onCommitFiberUnmount(_t,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),en(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=ft()-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;lft()-$c?Tl(e,0):Xc|=n),hl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=Tt,Tt<<=1,!(Tt&130023424)&&(Tt=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(vr(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 ct(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()}`),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()}`),bot:e=>D(`/api/bots/${e}`),collectibleUsernames:e=>D(`/api/collectible-usernames?${e.toString()}`),collectibleUsername:e=>D(`/api/collectible-usernames/${encodeURIComponent(e)}`),accountRatings:e=>D(`/api/account-ratings?${e.toString()}`),accountRating:e=>D(`/api/account-ratings/${encodeURIComponent(e)}`),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)}),gifts:()=>D(`/api/gifts`),officialGifts:()=>D(`/api/official-gifts`),officialGiftAnimation:e=>D(`/api/official-gifts/${encodeURIComponent(e)}/animation`),giftAnimation:e=>D(`/api/gifts/${encodeURIComponent(e)}/animation`),giftCollectibles:e=>D(`/api/gifts/${encodeURIComponent(e)}/collectibles`),giftCollectibleAnimation:(e,t,n)=>D(`/api/gifts/${encodeURIComponent(e)}/collectibles/${t}/${encodeURIComponent(n)}/animation`),importGift:e=>D(`/api/actions/import-gift`,{method:`POST`,body:e}),importOfficialGift:e=>D(`/api/actions/import-official-gift`,{method:`POST`,body:JSON.stringify(e)}),publishGiftCollectibles:(e,t)=>D(`/api/actions/publish-gift-collectibles?gift_id=${encodeURIComponent(e)}`,{method:`POST`,body:t}),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(`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`}]]),ae=P(`ArrowLeft`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]),oe=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`}]]),se=P(`Ban`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m4.9 4.9 14.2 14.2`,key:`1m5liu`}]]),ce=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`}]]),le=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`}]]),ue=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`}]]),de=P(`Calculator`,[[`rect`,{width:`16`,height:`20`,x:`4`,y:`2`,rx:`2`,key:`1nb95v`}],[`line`,{x1:`8`,x2:`16`,y1:`6`,y2:`6`,key:`x4nwl0`}],[`line`,{x1:`16`,x2:`16`,y1:`14`,y2:`18`,key:`wjye3r`}],[`path`,{d:`M16 10h.01`,key:`1m94wz`}],[`path`,{d:`M12 10h.01`,key:`1nrarc`}],[`path`,{d:`M8 10h.01`,key:`19clt8`}],[`path`,{d:`M12 14h.01`,key:`1etili`}],[`path`,{d:`M8 14h.01`,key:`6423bh`}],[`path`,{d:`M12 18h.01`,key:`mhygvu`}],[`path`,{d:`M8 18h.01`,key:`lrp35t`}]]),R=P(`Check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),fe=P(`ChevronDown`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),z=P(`ChevronRight`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),pe=P(`Clock3`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`polyline`,{points:`12 6 12 12 16.5 12`,key:`1aq6pp`}]]),me=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`}]]),he=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`}]]),ge=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`}]]),_e=P(`FileJson2`,[[`path`,{d:`M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4`,key:`1pf5j1`}],[`path`,{d:`M14 2v4a2 2 0 0 0 2 2h4`,key:`tnqrlb`}],[`path`,{d:`M4 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:`fq0c9t`}],[`path`,{d:`M8 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:`4gibmv`}]]),ve=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`}]]),ye=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`}]]),be=P(`Gem`,[[`path`,{d:`M6 3h12l4 6-10 13L2 9Z`,key:`1pcd5k`}],[`path`,{d:`M11 3 8 9l4 13 4-13-3-6`,key:`1fcu3u`}],[`path`,{d:`M2 9h20`,key:`16fsjt`}]]),xe=P(`Gift`,[[`rect`,{x:`3`,y:`8`,width:`18`,height:`4`,rx:`1`,key:`bkv52`}],[`path`,{d:`M12 8v13`,key:`1c76mn`}],[`path`,{d:`M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7`,key:`6wjy6b`}],[`path`,{d:`M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5`,key:`1ihvrl`}]]),B=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`}]]),Se=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`}]]),Ce=P(`KeyRound`,[[`path`,{d:`M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z`,key:`1s6t7t`}],[`circle`,{cx:`16.5`,cy:`7.5`,r:`.5`,fill:`currentColor`,key:`w0ekpg`}]]),we=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`}]]),Te=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`}]]),Ee=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`}]]),De=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`}]]),Oe=P(`Moon`,[[`path`,{d:`M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z`,key:`a7tn18`}]]),ke=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`}]]),Ae=P(`Pause`,[[`rect`,{x:`14`,y:`4`,width:`4`,height:`16`,rx:`1`,key:`zuxfzm`}],[`rect`,{x:`6`,y:`4`,width:`4`,height:`16`,rx:`1`,key:`1okwgv`}]]),je=P(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),Me=P(`Plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),Ne=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`}]]),Pe=P(`Power`,[[`path`,{d:`M12 2v10`,key:`mnfbl`}],[`path`,{d:`M18.4 6.6a9 9 0 1 1-12.77.04`,key:`obofu9`}]]),Fe=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`}]]),Ie=P(`Search`,[[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}],[`path`,{d:`m21 21-4.3-4.3`,key:`1qie3q`}]]),Le=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`}]]),Re=P(`Server`,[[`rect`,{width:`20`,height:`8`,x:`2`,y:`2`,rx:`2`,ry:`2`,key:`ngkwjq`}],[`rect`,{width:`20`,height:`8`,x:`2`,y:`14`,rx:`2`,ry:`2`,key:`iecqi9`}],[`line`,{x1:`6`,x2:`6.01`,y1:`6`,y2:`6`,key:`16zg32`}],[`line`,{x1:`6`,x2:`6.01`,y1:`18`,y2:`18`,key:`nzw8ys`}]]),ze=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`}]]),Be=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`}]]),Ve=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`}]]),He=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`}]]),Ue=P(`Shield`,[[`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`}]]),V=P(`SlidersHorizontal`,[[`line`,{x1:`21`,x2:`14`,y1:`4`,y2:`4`,key:`obuewd`}],[`line`,{x1:`10`,x2:`3`,y1:`4`,y2:`4`,key:`1q6298`}],[`line`,{x1:`21`,x2:`12`,y1:`12`,y2:`12`,key:`1iu8h1`}],[`line`,{x1:`8`,x2:`3`,y1:`12`,y2:`12`,key:`ntss68`}],[`line`,{x1:`21`,x2:`16`,y1:`20`,y2:`20`,key:`14d8ph`}],[`line`,{x1:`12`,x2:`3`,y1:`20`,y2:`20`,key:`m0wm8r`}],[`line`,{x1:`14`,x2:`14`,y1:`2`,y2:`6`,key:`14e1ph`}],[`line`,{x1:`8`,x2:`8`,y1:`10`,y2:`14`,key:`1i6ji0`}],[`line`,{x1:`16`,x2:`16`,y1:`18`,y2:`22`,key:`1lctlv`}]]),We=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`}]]),Ge=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`}]]),Ke=P(`Star`,[[`path`,{d:`M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z`,key:`r04s7s`}]]),qe=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`}]]),Je=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`}]]),Ye=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`}]]),Xe=P(`Trophy`,[[`path`,{d:`M6 9H4.5a2.5 2.5 0 0 1 0-5H6`,key:`17hqa7`}],[`path`,{d:`M18 9h1.5a2.5 2.5 0 0 0 0-5H18`,key:`lmptdp`}],[`path`,{d:`M4 22h16`,key:`57wxv0`}],[`path`,{d:`M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22`,key:`1nw9bq`}],[`path`,{d:`M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22`,key:`1np0yb`}],[`path`,{d:`M18 2H6v7a6 6 0 0 0 12 0V2Z`,key:`u46fv3`}]]),Ze=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`}]]),Qe=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`}]]),$e=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`}]]),et=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`}]]),tt=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`}]]),nt=P(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),rt=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})),H=o(((e,t)=>{t.exports=rt()}))(),it=`telesrv.admin.lang`,at={en:{"app.adminConsole":`Admin Console`,"app.localAccess":`Local access`,"app.title":`telesrv admin`,"common.actions":`Actions`,"common.admins":`Admins`,"common.backToList":`Back to list`,"common.channel":`Channel`,"common.channelOrGroup":`Channel / Group`,"common.clear":`Clear`,"common.close":`Close`,"common.count":`Count`,"common.deleted":`Deleted`,"common.detail":`Details`,"common.device":`Device`,"common.disabled":`Disabled`,"common.enabled":`Enabled`,"common.fromPeer":`From Peer`,"common.group":`Group`,"common.id":`ID`,"common.limit":`Limit`,"common.loading":`Loading`,"common.member":`Member`,"common.members":`Members`,"common.messageId":`Message ID`,"common.name":`Name`,"common.no":`No`,"common.noResults":`No results`,"common.none":`None`,"common.normal":`Normal`,"common.operations":`Operations`,"common.owner":`Owner`,"common.platform":`Platform`,"common.refresh":`Refresh`,"common.search":`Search`,"common.sender":`Sender`,"common.status":`Status`,"common.survived":`Live`,"common.time":`Time`,"common.type":`Type`,"common.updatedAt":`Updated`,"common.username":`Username`,"common.valid":`Valid`,"common.verified":`Verified`,"common.views":`Views`,"common.yes":`Yes`,"route.accounts":`Accounts`,"route.accountsSubtitle":`Console / Accounts`,"route.channels":`Supergroups and Channels`,"route.channelsSubtitle":`Console / Channels`,"route.moderation":`Reports and Moderation`,"route.moderationSubtitle":`Console / Moderation`,"route.dashboard":`Operations Console`,"route.dashboardSubtitle":`Console / Overview`,"route.messages":`Message Audit`,"route.messagesSubtitle":`Console / Messages`,"route.gifts":`Star Gifts`,"route.giftsSubtitle":`Console / Star Gifts`,"route.giveGifts":`Give Gifts`,"route.giveGiftsSubtitle":`Console / Give Gifts`,"layout.navigation":`Navigation`,"layout.primaryNav":`Primary navigation`,"layout.dashboard":`Overview`,"layout.accounts":`Accounts`,"layout.channels":`Supergroups / Channels`,"layout.moderation":`Reports / Moderation`,"layout.messages":`Messages`,"layout.gifts":`Star Gifts`,"layout.giveGifts":`Give Gifts`,"layout.privateMessages":`Private`,"layout.groupMessages":`Groups`,"layout.runtime":`Runtime`,"layout.adminBackend":`Admin backend`,"layout.ready":`Ready`,"layout.pgRead":`PG read`,"layout.readOnly":`Read-only`,"layout.writeOps":`Write operations`,"layout.dryRun":`Dry-run`,"layout.actor":`Actor: {actor}`,"layout.logout":`Log out`,"language.en":`EN`,"language.zh":`中文`,"language.ru":`RU`,"theme.switchToDark":`Switch to dark theme`,"theme.switchToLight":`Switch to light theme`,"login.heading":`Operations Admin`,"login.body":`Enter credentials to open the console.`,"login.secret":`Admin password or token`,"login.submit":`Log in`,"login.submitting":`Logging in`,"dashboard.eyebrow":`Runtime Overview`,"dashboard.title":`Console Overview`,"dashboard.readPath":`Read path`,"dashboard.readPathValue":`PG read-only`,"dashboard.writePath":`Write path`,"dashboard.executionPolicy":`Execution policy`,"dashboard.dryRunFirst":`Dry-run first`,"dashboard.accountsText":`Account status, premium, verification, sessions.`,"dashboard.channelsText":`Public entities, member counts, verification state.`,"dashboard.messagesText":`Message boxes, updates, outbox state.`,"dashboard.strip.dryRun":`All dangerous actions start with dry-run`,"dashboard.strip.token":`Browser never stores internal tokens`,"dashboard.strip.pagination":`Lists use cursor pagination`,"dashboard.strip.snapshot":`Detail pages retain raw state snapshots`,"account.pageTitle":`Accounts`,"account.queryResults":`Search results`,"account.recentActive":`Recently active accounts`,"account.currentPage":`Accounts on page`,"account.onlineDevices":`Online device records`,"account.premium":`Premium`,"account.frozen":`Frozen`,"account.searchPlaceholder":`User ID / phone / username`,"account.userID":`User ID`,"account.phone":`Phone`,"account.lastActive":`Last active`,"account.notVerified":`Not verified`,"account.notPremium":`Not premium`,"account.premiumUntil":`Premium expires`,"account.starsBalance":`Stars balance`,"account.startingGrantApplied":`initial grant applied`,"account.startingGrantPending":`initial grant pending`,"account.activeSessions":`Authorized devices`,"account.accountFlags":`Account flags`,"account.restriction":`Restriction`,"account.restricted":`Restricted`,"account.createdAt":`Created`,"account.detailTitle":`Account #{id}`,"account.profile":`Account Profile`,"account.loadingDetail":`Loading account detail`,"account.waitingData":`Waiting for data`,"account.noUsername":`No username`,"account.noPhone":`No phone`,"account.accountFrozen":`Account frozen`,"account.accountActive":`Account active`,"account.authorizationsTitle":`Authorized Devices`,"account.authorizationsCount":`{count} authorizations`,"account.recentAdminOps":`Recent Admin Actions`,"account.recent30Audit":`Last 30 audit rows`,"account.actionDock":`Account Actions`,"account.freezeAccount":`Freeze account`,"account.updateFreeze":`Update freeze`,"account.unfreezeAccount":`Unfreeze account`,"account.freezeSince":`Frozen since`,"account.freezeUntil":`Appeal deadline`,"account.freezeUntilAria":`Freeze appeal deadline`,"account.freezeAppealURL":`Appeal URL`,"account.freezeAppealURLAria":`Freeze appeal URL`,"account.premiumMonths":`Premium duration (months)`,"account.premiumMonthsAria":`Set premium duration in months`,"account.setPremium":`Set premium`,"account.clearPremium":`Clear premium`,"account.starsAmount":`Stars to grant`,"account.starsAmountAria":`Set Stars amount to grant`,"account.grantStars":`Grant Stars`,"account.setVerified":`Set verified`,"account.clearVerified":`Clear verified`,"channel.pageTitle":`Supergroups and Channels`,"channel.recentUpdated":`Recently updated`,"channel.currentPage":`Entities on page`,"channel.megagroups":`Supergroups`,"channel.broadcasts":`Channels`,"channel.verifiedCount":`Verified`,"channel.searchPlaceholder":`Channel ID / username / title`,"channel.channelID":`Channel ID`,"channel.kind":`Kind`,"channel.title":`Title`,"channel.pts":`PTS`,"channel.detailProfile":`Channel Profile`,"channel.loadingDetail":`Loading channel detail`,"channel.creator":`Creator {id}`,"channel.governance":`Moderation`,"channel.governanceValue":`Banned {banned} / Kicked {kicked}`,"channel.flags":`Channel flags`,"channel.rawRow":`Channel Raw Row`,"channel.rawRowText":`Database read-only snapshot`,"channel.actionDock":`Channel Actions`,"channel.setVerified":`Set verified`,"channel.clearVerified":`Clear verified`,"channel.kind.broadcast":`Channel`,"channel.kind.forum":`Supergroup / Forum`,"channel.kind.megagroup":`Supergroup`,"channel.kind.generic":`Channel / Group`,"route.bots":`Bots`,"route.botsSubtitle":`Console / Bots`,"layout.bots":`Bots`,"bots.pageTitle":`Bots`,"bots.queryResults":`Search results`,"bots.recent":`Recently created bots`,"bots.currentPage":`Bots on page`,"bots.banned":`Banned`,"bots.active":`Active`,"bots.createTitle":`Create a system bot`,"bots.createHint":`Provision a bot account owned by the given user. The token is shown once after confirmation.`,"bots.ownerUserID":`Owner user ID`,"bots.name":`Display name`,"bots.namePlaceholder":`e.g. Service Bot`,"bots.username":`Username`,"bots.usernameHint":`Username must be 5-32 characters and end with 'bot'.`,"bots.create":`Create bot`,"bots.searchPlaceholder":`Bot ID / username`,"bots.botID":`Bot ID`,"bots.owner":`Owner`,"bots.status":`Status`,"bots.detailTitle":`Bot #{id}`,"bots.profile":`Bot Profile`,"bots.loadingDetail":`Loading bot detail`,"bots.unnamed":`Unnamed bot`,"bots.restriction":`Restriction`,"bots.actionDock":`Bot Actions`,"bots.banUntil":`Ban until`,"bots.ban":`Ban bot`,"bots.updateBan":`Update ban`,"bots.unban":`Unban bot`,"bots.type":`Type`,"bots.system":`System`,"bots.user":`User`,"bots.delete":`Delete bot`,"bots.deleteHint":`Permanently deletes this user-created bot and invalidates its token. This cannot be undone.`,"bots.systemHint":`System bots are built in and cannot be deleted.`,"flags.scam":`SCAM`,"flags.fake":`FAKE`,"flags.setScam":`Mark as SCAM`,"flags.clearScam":`Clear SCAM`,"flags.setFake":`Mark as FAKE`,"flags.clearFake":`Clear FAKE`,"attr.attributes":`Attributes`,"attr.settings":`Settings`,"attr.username":`Username`,"attr.setUsername":`Set username`,"attr.setSupport":`Mark as support`,"attr.clearSupport":`Clear support`,"attr.forProfile":`Profile color`,"attr.hasColor":`Enable color`,"attr.colorIndex":`Color index`,"attr.bgEmojiID":`Background emoji ID`,"attr.setColor":`Set color`,"attr.emojiDocID":`Emoji document ID`,"attr.emojiUntil":`Until (unix, 0 = permanent)`,"attr.setEmojiStatus":`Set emoji status`,"attr.gigagroup":`Gigagroup`,"attr.antispam":`Aggressive anti-spam`,"attr.participantsHidden":`Hide members`,"attr.noforwards":`Restrict forwarding`,"attr.joinToSend":`Join to send messages`,"attr.joinRequest":`Join by request`,"attr.slowmode":`Slowmode (seconds)`,"attr.applySettings":`Apply settings`,"route.emoji":`Emoji`,"route.emojiSubtitle":`Console / Emoji`,"layout.emoji":`Emoji`,"emoji.pageTitle":`Custom Emoji`,"emoji.queryResults":`Search results`,"emoji.recent":`Custom emoji catalog`,"emoji.currentPage":`Emoji on page`,"emoji.searchPlaceholder":`Document ID or emoji`,"emoji.copyID":`Copy document ID`,"emoji.noSet":`No set`,"emoji.hint":`Document IDs here can be pasted into the Emoji status field on account, bot and channel profiles.`,"messages.privateTitle":`Private Messages`,"messages.privateEyebrow":`Private message boxes`,"messages.groupTitle":`Group Messages`,"messages.groupEyebrow":`Supergroup / channel messages`,"messages.selectPrivatePeers":`Search and select the owner user and peer user first`,"messages.selectChannel":`Search and select a supergroup or channel first`,"messages.ownerUser":`Owner user`,"messages.peerUser":`Peer user`,"messages.beforeDatePlaceholder":`before_date cursor`,"messages.beforeIDPlaceholder":`before_msg_id cursor`,"messages.limitPlaceholder":`limit <= 100`,"messages.searchMessages":`Search messages`,"messages.nextPage":`Next page`,"messages.currentPage":`Messages on page`,"messages.deleted":`Deleted`,"messages.outgoing":`Outgoing`,"messages.incoming":`Incoming`,"messages.ownerPeer":`Owner / Peer`,"messages.deleteSelected":`Delete selected messages`,"messages.idsPlaceholder":`Message IDs, comma separated`,"messages.revoke":`Revoke for both sides`,"messages.previewDelete":`Dry-run delete`,"messages.clearHistory":`Clear private history`,"messages.maxIDPlaceholder":`max_id cutoff`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Clear only this side`,"messages.previewClearHistory":`Dry-run clear history`,"messages.direction":`Direction`,"messages.body":`Body`,"messages.privateDetailTitle":`Message #{id}`,"messages.detailEyebrow":`Message Detail`,"messages.backPrivate":`Back to private messages`,"messages.backGroup":`Back to group messages`,"messages.ownerPeerTitle":`Owner {owner} · Peer {peer}`,"messages.senderSubtitle":`Sender {sender} · {date}`,"messages.boxID":`Message box ID`,"messages.privateMessageID":`Private message ID`,"messages.messageSender":`Message sender`,"messages.messageBox":`Message Box`,"messages.dialogRow":`Dialog Row`,"messages.privateRow":`Private Message Row`,"messages.channelMessageRow":`Channel Message Row`,"messages.channelRow":`Channel Row`,"messages.userUpdateEvents":`Update Events`,"messages.channelUpdateEvents":`Channel Update Events`,"messages.eventJson":`Event JSON`,"messages.dispatchOutbox":`Dispatch Queue`,"messages.messageBoxesSnapshot":`message_boxes read-only snapshot`,"messages.dialogSnapshot":`dialogs read-only snapshot`,"messages.privateSnapshot":`private_messages read-only snapshot`,"messages.channelMessagesSnapshot":`channel_messages read-only snapshot`,"messages.channelSnapshot":`channels read-only snapshot`,"messages.userEventsSource":`durable user_update_events`,"messages.channelEventsSource":`durable channel_update_events`,"messages.outboxSource":`online/offline dispatch_outbox`,"messages.attempts":`Attempts`,"messages.deleteThis":`Delete this message`,"messages.groupDetailTitle":`Group Message #{id}`,"messages.channelGroupTitle":`Channel / Group {id}`,"messages.mediaCount":`With media`,"messages.channelPosts":`Channel posts`,"messages.channelGroup":`Channel / Group`,"messages.pinned":`Pinned`,"messages.channelPost":`Channel post`,"gifts.pageTitle":`Star Gift Catalog`,"giveGift.action":`Give`,"giveGift.eyebrow":`Grant a gift · no charge`,"giveGift.title":`Give gift`,"giveGift.recipientKind":`Recipient type`,"giveGift.recipientUser":`User`,"giveGift.recipientChannel":`Channel`,"giveGift.pickUser":`Recipient user`,"giveGift.pickChannel":`Recipient channel`,"giveGift.recipientRequired":`Select a recipient first`,"giveGift.sender":`Sender account ID`,"giveGift.senderHint":`Gifts are always sent from the system account 777000 (Telesrv).`,"giveGift.message":`Attached message (optional)`,"giveGift.messagePlaceholder":`Shown with the gift`,"giveGift.hideName":`Hide sender name from recipient`,"giveGift.upgrade":`Deliver as upgraded collectible`,"giveGift.upgradeNote":`The gift is minted as a unique collectible. Pick specific attributes below, or leave them on Random to draw from the published pool. The collectible number is assigned automatically. Requires a published collectible upgrade with remaining supply.`,"giveGift.model":`Model`,"giveGift.pattern":`Pattern`,"giveGift.backdrop":`Backdrop`,"giveGift.random":`Random`,"giveGift.confirm":`Give gift`,"giveGifts.pageTitle":`Give Gifts`,"giveGifts.eyebrow":`Grant catalog gifts to any user or channel`,"giveGifts.available":`Available gifts`,"giveGifts.sender":`Default sender`,"giveGifts.searchPlaceholder":`Search by title or gift ID`,"giveGifts.hint":`Pick a gift to grant. Delivery is free of charge and sent from the system account 777000 (Telesrv) by default.`,"giveGifts.pickGift":`Select a gift`,"giveGifts.selectPrompt":`Select a gift from the list to start.`,"gifts.eyebrow":`Catalog, immutable revisions and animation assets`,"gifts.total":`Catalog entries`,"gifts.enabled":`Enabled`,"gifts.received":`Received gifts`,"gifts.formats":`Accepted formats`,"gifts.add":`Add gift`,"gifts.searchPlaceholder":`Search gift ID, title or format`,"gifts.listSummary":`Showing {shown} of {total}`,"gifts.idRevision":`ID / Revision`,"gifts.price":`Price / Conversion`,"gifts.importTitle":`Import a Star Gift`,"gifts.importEyebrow":`Gift catalog operation`,"gifts.newRevision":`Create revision for gift #{id}`,"gifts.importHint":`Upload TGS or plain Lottie JSON. Lottie is normalized and compressed to TGS.`,"gifts.officialSource":`Official snapshot`,"gifts.fileSource":`Upload file`,"gifts.officialHint":`Choose a verified gift from data/official-gifts. Complete collectible pools are imported atomically.`,"gifts.officialSearch":`Search official gift ID or title`,"gifts.officialSelect":`Choose an official gift`,"gifts.officialRequired":`Choose an official gift first`,"gifts.officialResults":`Showing {shown} of {total}`,"gifts.officialCategoryLabel":`Official gift capability category`,"gifts.officialCategory.all":`All`,"gifts.officialCategory.upgrade":`Upgradable`,"gifts.officialCategory.craft":`Craftable`,"gifts.officialCategory.basic":`Not upgradable`,"gifts.officialUnnamed":`Unnamed official gift #{id}`,"gifts.officialAttributes":`{count} attributes`,"gifts.canUpgrade":`Can upgrade`,"gifts.cannotUpgrade":`Cannot upgrade`,"gifts.canCraft":`Can Craft`,"gifts.cannotCraft":`Cannot Craft`,"gifts.officialEmpty":`No official gifts match this category and search.`,"gifts.includeCollectible":`Import the complete collectible pool, including crafted models`,"gifts.animation":`Animation file`,"gifts.filePrompt":`Drop or choose a TGS / Lottie file`,"gifts.fileHint":`TGS, JSON or Lottie · validated before import`,"gifts.chooseFile":`Choose file`,"gifts.changeFile":`Change file`,"gifts.title":`Display title`,"gifts.titlePlaceholder":`e.g. Celebration Star`,"gifts.stars":`Price in Stars`,"gifts.convertStars":`Conversion Stars`,"gifts.sortOrder":`Sort order`,"gifts.reason":`Audit reason`,"gifts.reasonPlaceholder":`Briefly describe why this gift is being imported`,"gifts.enableAfterImport":`Enable after import`,"gifts.validate":`Dry-run validation`,"gifts.confirmImport":`Confirm import`,"gifts.stepDetails":`File and details`,"gifts.stepValidate":`Dry-run validation`,"gifts.stepImport":`Confirm import`,"gifts.fileRequired":`Choose a TGS or Lottie file first`,"gifts.source":`Source`,"gifts.replace":`New revision`,"gifts.disable":`Disable`,"gifts.enable":`Enable`,"gifts.empty":`No Star Gifts have been imported.`,"gifts.emptyHint":`Import the first animation above to build the gift catalog.`,"gifts.validationReady":`Validation passed`,"gifts.validationHint":`Review the normalized metadata, then confirm the import.`,"gifts.confirmState":`Apply the validated state change to gift #{id}?`,"collectibles.manage":`Attribute pool`,"collectibles.title":`Collectible pool · Gift #{id}`,"collectibles.eyebrow":`Unique gift attributes`,"collectibles.activeRevision":`Published revision {revision}`,"collectibles.published":`Published`,"collectibles.noPool":`No collectible pool published`,"collectibles.noPoolHint":`Publish models, patterns and backdrops to enable upgrades.`,"collectibles.publishNew":`Publish a new immutable revision`,"collectibles.immutableHint":`Dry-run checks every file and the attribute-pool structure before the revision becomes active.`,"collectibles.upgradeStars":`Upgrade price in Stars`,"collectibles.supply":`Unique supply`,"collectibles.slug":`Public slug prefix`,"collectibles.models":`Models`,"collectibles.patterns":`Patterns`,"collectibles.backdrops":`Backdrops`,"collectibles.model":`Model`,"collectibles.pattern":`Pattern`,"collectibles.backdrop":`Backdrop`,"collectibles.rarity":`Rarity ‰`,"collectibles.rarityHint":`Keep at least two attributes. Permille values are relative regular-upgrade weights; add/remove redistributes them to 1000.`,"collectibles.minimumAttributes":`Models, patterns, and backdrops must each contain at least two attributes.`,"collectibles.duplicateBackdropID":`Backdrop IDs must be unique within the pool.`,"collectibles.colorHint":`Colors are stored as 24-bit RGB values.`,"collectibles.addAttribute":`Add`,"collectibles.remove":`Remove attribute`,"collectibles.fileRequired":`Every model and pattern needs a TGS or Lottie file.`,"collectibles.backdropID":`Backdrop ID`,"collectibles.color.center":`Center`,"collectibles.color.edge":`Edge`,"collectibles.color.pattern":`Pattern`,"collectibles.color.text":`Text`,"collectibles.validationReady":`Attribute pool is valid`,"collectibles.validationHint":`Review the normalized assets, then publish this immutable revision.`,"collectibles.publish":`Publish revision`,"moderation.casesEyebrow":`Moderation / Cases`,"moderation.currentQueue":`Current queue`,"moderation.criticalCases":`Critical cases`,"moderation.pendingOrFailed":`Pending / failed actions`,"moderation.statusFilter":`Case status filter`,"moderation.statusFilter.active":`Active queue`,"moderation.statusFilter.all":`All statuses`,"moderation.assignee":`Reviewer`,"moderation.allAssignees":`Leave blank for all`,"moderation.case":`Case`,"moderation.target":`Target`,"moderation.severity":`Severity`,"moderation.reportsAndReporters":`Reports / Reporters`,"moderation.latestReport":`Latest report`,"moderation.review":`Review`,"moderation.status.open":`Open`,"moderation.status.in_review":`In review`,"moderation.status.action_pending":`Action pending`,"moderation.status.action_failed":`Action failed`,"moderation.status.resolved":`Resolved`,"moderation.status.dismissed":`Dismissed`,"moderation.status.appeal_review":`Appeal review`,"moderation.severity.low":`Low`,"moderation.severity.medium":`Medium`,"moderation.severity.high":`High`,"moderation.severity.critical":`Critical`,"moderation.targetType.user":`Account`,"moderation.targetType.chat":`Group`,"moderation.targetType.channel":`Channel`,"moderation.caseDetailTitle":`Review case #{id}`,"moderation.caseDetailEyebrow":`Moderation / Case detail`,"moderation.backToQueue":`Back to queue`,"moderation.loadingCase":`Loading moderation case…`,"moderation.versionAndUpdated":`Version {version} · Updated {time}`,"moderation.reportCount":`Reports`,"moderation.reportCountValue":`{reports} reports from {reporters} reporters`,"moderation.firstAndLatestReport":`First / latest report`,"moderation.evidence":`Report evidence`,"moderation.evidenceHint":`Shows up to the latest 100 reports; snapshots are frozen when reports are admitted.`,"moderation.sourceAndReason":`Source / Reason`,"moderation.reporter":`Reporter`,"moderation.option":`Option`,"moderation.decisionAudit":`Decision and action audit`,"moderation.decisionAuditHint":`Actions run idempotently through a lease worker; failures retain their error and attempt count.`,"moderation.appeals":`Appeals`,"moderation.caseActions":`Case actions`,"moderation.renewClaim":`Renew claim`,"moderation.claimCase":`Claim case`,"moderation.reviewReason":`Review reason`,"moderation.decisionPreset":`Decision template`,"moderation.preset.noViolation":`No violation (dismiss report)`,"moderation.preset.scam":`Mark as SCAM`,"moderation.preset.fake":`Mark as FAKE`,"moderation.preset.freeze":`Freeze account`,"moderation.preset.scamFreeze":`SCAM + freeze`,"moderation.preset.fakeFreeze":`FAKE + freeze`,"moderation.preset.deleteMessages":`Delete messages covered by evidence`,"moderation.preset.deleteAccount":`Delete account`,"moderation.evidenceMessageIDs":`Evidence message IDs (comma-separated)`,"moderation.privateOwnerUserID":`Private-chat owner_user_id`,"moderation.revokeForBoth":`Revoke for both sides`,"moderation.evidenceValidationHint":`The server will verify again that every message ID exists in this case's immutable report evidence.`,"moderation.failedActionHint":`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.`,"moderation.retryAction":`Retry action`,"moderation.submitDecision":`Submit decision`,"moderation.appealReviewTitle":`Appeal review #{id}`,"moderation.automaticRemedy":`Automatic remedy after approval`,"moderation.irreversibleAppealHint":`The case contains a completed irreversible deletion. It cannot be marked as approved and restored; deny it or escalate for manual handling.`,"moderation.denyAppeal":`Deny appeal`,"moderation.grantAppeal":`Grant appeal`,"moderation.reasonRequired":`A review reason is required.`,"moderation.appealReasonRequired":`An appeal review reason is required.`,"moderation.privateDeleteValidation":`Private-message deletion requires valid evidence message IDs and the reporter's owner_user_id.`,"moderation.channelDeleteValidation":`Channel-message deletion requires at least one valid evidence message ID.`,"moderation.confirmDecision":`Submit the “{decision}” decision? The action will run through the durable action queue.`,"moderation.confirmGrantAppeal":`Grant this appeal?`,"moderation.confirmDenyAppeal":`Deny this appeal?`,"moderation.remedy.clearFlags":`Clear SCAM / FAKE`,"moderation.remedy.unfreeze":`Unfreeze account`,"moderation.remedy.none":`No recovery action needed`,"moderation.source.account_peer":`Account / peer`,"moderation.source.profile_photo":`Profile photo`,"moderation.source.messages_spam":`Message spam`,"moderation.source.messages":`Messages`,"moderation.source.encrypted_spam":`Encrypted-chat spam`,"moderation.source.reaction":`Reaction`,"moderation.source.channel_spam":`Channel spam`,"moderation.source.story":`Story`,"moderation.source.ephemeral":`Ephemeral media`,"moderation.source.sponsored":`Sponsored message`,"moderation.source.antispam_false_positive":`Anti-spam false positive`,"moderation.reason.spam":`Spam`,"moderation.reason.violence":`Violence`,"moderation.reason.pornography":`Pornography`,"moderation.reason.child_abuse":`Child abuse`,"moderation.reason.other":`Other`,"moderation.reason.copyright":`Copyright`,"moderation.reason.geo_irrelevant":`Location-irrelevant`,"moderation.reason.fake":`Fake`,"moderation.reason.illegal_drugs":`Illegal drugs`,"moderation.reason.personal_details":`Personal details`,"messages.msgIDsInvalid":`Message IDs are invalid`,"auth.device":`Device`,"auth.platform":`Platform`,"auth.ip":`IP`,"auth.lastActive":`Last active`,"auth.revokeCurrent":`Revoke current`,"auth.keepCurrent":`Keep current`,"auth.revokeAll":`Revoke all devices`,"picker.userPlaceholder":`Search user_id / phone / username`,"picker.channelPlaceholder":`Search channel_id / username / title`,"picker.verified":`Verified`,"picker.regular":`Regular`,"action.reasonRequired":`Please enter an operation reason`,"action.flow":`Action Flow`,"action.close":`Close`,"action.stepReason":`Enter reason`,"action.stepDryRun":`Dry-run check`,"action.stepConfirm":`Confirm execution`,"action.reason":`Operation reason`,"action.reasonPlaceholder":`Describe why this operation is being performed`,"action.requestPreview":`Request preview`,"action.result":`Action result`,"action.commandID":`Command ID`,"action.status":`Status`,"action.dryRun":`Dry-run`,"action.runAgain":`Run dry-run again`,"action.runDry":`Run dry-run first`,"action.confirm":`Confirm execution`,"audit.id":`ID`,"audit.commandID":`Command ID`,"audit.action":`Action`,"audit.actor":`Actor`,"audit.status":`Status`,"audit.dryRun":`Dry-run`,"audit.reason":`Reason`,"audit.time":`Time`,"common.loadMore":`Load more`,"route.collectibleUsernames":`Collectible Usernames`,"route.collectibleUsernamesSubtitle":`Console / Collectible usernames`,"route.accountRatings":`Account Rating`,"route.accountRatingsSubtitle":`Console / Account rating`,"layout.collectibleUsernames":`NFT Usernames`,"layout.accountRatings":`Account Rating`,"usernames.pageTitle":`Collectible usernames`,"usernames.eyebrow":`NFT usernames / Registry`,"usernames.metricLoaded":`Loaded rows`,"usernames.metricVault":`In vault`,"usernames.metricOwned":`Held by owners`,"usernames.metricBurned":`Burned`,"usernames.mintTitle":`Mint a collectible username`,"usernames.mintHint":`Creates the asset together with its purchase record. Keep the owner as vault to mint it unassigned.`,"usernames.mint":`Mint username`,"usernames.mintNote":`Username, currency and amount are required; the dry-run checks availability first.`,"usernames.ownerKind":`Owner type`,"usernames.ownerVault":`Vault (no owner)`,"usernames.ownerUser":`User owner`,"usernames.ownerChannel":`Channel owner`,"usernames.currency":`Currency`,"usernames.amount":`Amount ({currency})`,"usernames.cryptoCurrency":`Crypto currency`,"usernames.cryptoNone":`None`,"usernames.cryptoAmount":`Crypto amount ({currency})`,"usernames.amountHint":`Amounts are typed in whole {currency} and stored as the smallest units the API and fragment.collectibleInfo carry, so clients render the price you meant. Up to {decimals} decimal places. Clients will show: {preview}.`,"usernames.amountInvalid":`That is not a valid {currency} amount: digits only, with at most {decimals} decimal places.`,"usernames.inactive":`inactive`,"usernames.url":`Marketplace URL`,"usernames.purchaseDate":`Purchase date (UTC)`,"usernames.purchaseTime":`Purchase time (UTC)`,"usernames.searchPlaceholder":`Search by username`,"usernames.statusAll":`All statuses`,"usernames.statusVault":`Vault`,"usernames.statusOwned":`Owned`,"usernames.statusBurned":`Burned`,"usernames.price":`Price`,"usernames.transfers":`Transfers`,"usernames.registryActive":`Active in profile`,"usernames.registryHidden":`Hidden in profile`,"usernames.loadingDetail":`Loading collectible username…`,"usernames.detailTitle":`Collectible {username}`,"usernames.detailEyebrow":`NFT usernames / Asset`,"usernames.assetID":`Asset #{id}`,"usernames.transferCount":`{count} transfers`,"usernames.originalOwner":`Original owner`,"usernames.openOwnerAccount":`Open owner account`,"usernames.openOwnerChannel":`Open owner channel`,"usernames.openMarketplace":`Open marketplace page`,"usernames.transferTitle":`Transfer ownership`,"usernames.transferHint":`Pick the recipient; the transfer is appended to the provenance history.`,"usernames.recipientKind":`Recipient type`,"usernames.recipientUser":`To user`,"usernames.recipientChannel":`To channel`,"usernames.transferNote":`The current owner loses the username immediately after confirmation.`,"usernames.transfer":`Transfer`,"usernames.historyTitle":`Provenance history`,"usernames.historyHint":`Mint, transfer, revoke and burn events in chronological order.`,"usernames.eventKind":`Event`,"usernames.fromPeer":`From`,"usernames.toPeer":`To`,"usernames.actionDock":`Asset operations`,"usernames.revoke":`Revoke to vault`,"usernames.revokeHint":`Takes the username away from its owner and returns it to the vault; it can be issued again later.`,"usernames.burn":`Burn permanently`,"usernames.burnHint":`Irreversible: the username is destroyed and can never be issued again.`,"usernames.burnedHint":`This username is burned — no further operations are possible.`,"usernames.delete":`Delete record`,"usernames.deleteHint":`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.`,"usernames.kind.mint":`Mint`,"usernames.kind.transfer":`Transfer`,"usernames.kind.revoke":`Revoke`,"usernames.kind.burn":`Burn`,"rating.pageTitle":`Account rating leaderboard`,"rating.eyebrow":`Rating / Leaderboard`,"rating.metricLoaded":`Loaded rows`,"rating.metricTopLevel":`Top level`,"rating.metricAvgLevel":`Average level`,"rating.metricPending":`With pending points`,"rating.searchPlaceholder":`Search by username, name or user ID`,"rating.minLevel":`Min level`,"rating.userID":`User ID`,"rating.level":`Level`,"rating.stars":`Points`,"rating.progress":`Progress to next level`,"rating.pending":`Pending`,"rating.computedAt":`Computed`,"rating.levelValue":`Level {level}`,"rating.maxLevel":`Max level reached`,"rating.progressHint":`{remaining} left to reach {target}`,"rating.loadingDetail":`Loading account rating…`,"rating.detailTitle":`Rating of {user}`,"rating.detailEyebrow":`Rating / Component breakdown`,"rating.pendingBadge":`Pending {amount}`,"rating.nextLevel":`Next level threshold`,"rating.toNextLevel":`Points to next level`,"rating.breakdownTitle":`How the rating adds up`,"rating.breakdownHint":`Contribution of every source: stars, activity, moderation penalties and manual corrections.`,"rating.breakdownMismatch":`Components add up to {sum} while the stored rating is {total}. Recompute to resolve the drift.`,"rating.breakdownPending":`Components already include {amount} that reaches the score only on the date below.`,"rating.currentLevelStars":`Current level threshold`,"rating.nextLevelStars":`Next level threshold`,"rating.pendingTitle":`Pending points`,"rating.pendingHint":`Already earned, but counted towards the rating only on the date below.`,"rating.pendingDate":`Applied on`,"rating.eventsTitle":`Rating events`,"rating.eventsHint":`Every rating change with its source, actor and reason.`,"rating.eventKind":`Source`,"rating.amount":`Change`,"rating.actionDock":`Rating operations`,"rating.openAccount":`Open account`,"rating.recompute":`Recompute`,"rating.recomputeHint":`Rebuilds the rating from stars, activity, penalties and manual corrections.`,"rating.adjustTitle":`Manual correction`,"rating.adjustAmount":`Value (negative allowed)`,"rating.adjust":`Apply correction`,"rating.adjustHint":`The value is added to the manual component; a negative number lowers the rating.`,"rating.componentStars":`Stars`,"rating.componentStarsHint":`Purchased and received stars`,"rating.componentActivity":`Activity`,"rating.componentActivityHint":`Messages, sessions and long-term engagement`,"rating.componentPenalty":`Penalties`,"rating.componentPenaltyHint":`Moderation decisions and restrictions`,"rating.componentManual":`Manual corrections`,"rating.componentManualHint":`Adjustments made by admins`,"rating.componentTotal":`Total rating`,"rating.kind.stars":`Stars`,"rating.kind.activity":`Activity`,"rating.kind.moderation":`Moderation`,"rating.kind.manual":`Manual`,"rating.kind.recompute":`Recompute`,"route.verification":`Official Verification`,"route.verificationSubtitle":`Console / Verification`,"layout.verification":`Verification`,"permission.deniedTitle":`Not enough rights`,"permission.deniedEyebrow":`Console / Access`,"permission.deniedBody":`This session was not granted the {permission} permission, so the section stays closed.`,"permission.deniedHeading":`Section unavailable`,"permission.deniedHint":`Ask an operator to add the permission to TELESRV_ADMIN_UI_PERMISSIONS and sign in again.`,"verification.pageTitle":`Verification queue`,"verification.eyebrow":`Verification / Queue`,"verification.searchPlaceholder":`Application id, peer id, username or title`,"verification.statusAll":`All statuses`,"verification.targetType":`Target type`,"verification.targetTypeAll":`All types`,"verification.reviewer":`Reviewer`,"verification.reviewerPlaceholder":`Any reviewer`,"verification.target":`Target`,"verification.applicant":`Applicant`,"verification.category":`Category`,"verification.submittedAt":`Submitted`,"verification.alreadyVerified":`Badge already on`,"verification.status.draft":`Draft`,"verification.status.submitted":`Submitted`,"verification.status.in_review":`In review`,"verification.status.approved":`Approved`,"verification.status.rejected":`Rejected`,"verification.status.cancelled":`Cancelled`,"verification.type.bot":`Bot`,"verification.type.channel":`Channel`,"verification.type.supergroup":`Supergroup`,"verification.type.user":`User`,"verification.loadingDetail":`Loading the application…`,"verification.detailTitle":`Application #{id}`,"verification.detailEyebrow":`Verification / Review`,"verification.conflict":`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`,"verification.controlsOk":`Control confirmed`,"verification.controlsLost":`No control over the target`,"verification.controlsOkHint":`The applicant controls the target right now — checked against the live records, not against the submission snapshot.`,"verification.controlsLostHint":`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.`,"verification.targetSection":`Target`,"verification.targetHint":`The peer the badge would be attached to, as it exists right now.`,"verification.openTarget":`Open target`,"verification.targetTitle":`Title`,"verification.targetID":`Peer ID`,"verification.applicantSection":`Applicant`,"verification.applicantHint":`Who filed the application and whether they still hold rights on the target.`,"verification.openApplicant":`Open account`,"verification.applicantID":`User ID`,"verification.applicationSection":`Application`,"verification.applicationHint":`Everything the applicant submitted, rendered as plain text.`,"verification.correlationID":`Correlation ID`,"verification.createdAt":`Created`,"verification.description":`Description`,"verification.officialWebsite":`Official website`,"verification.socialLinks":`Social links`,"verification.pressLinks":`Press coverage`,"verification.additionalNote":`Applicant comment`,"verification.notProvided":`Not provided`,"verification.linkSafetyHint":`Only http:// and https:// links are clickable and open in a new tab; anything else is shown as text.`,"verification.decisionSection":`Decision`,"verification.decisionHint":`What was decided, by whom, and with which wording.`,"verification.reviewedAt":`Decided`,"verification.version":`Version (optimistic lock)`,"verification.decisionReason":`Decision reason`,"verification.noDecision":`No decision yet`,"verification.internalNote":`Internal note`,"verification.adminOnly":`admins only`,"verification.eventsSection":`History`,"verification.eventsHint":`Immutable trail of every status transition, with actor and reason.`,"verification.eventKind":`Event`,"verification.transition":`From → to`,"verification.eventNote":`Internal note`,"verification.kind.created":`Created`,"verification.kind.updated":`Updated`,"verification.kind.submitted":`Submitted`,"verification.kind.claimed":`Claimed`,"verification.kind.approved":`Approved`,"verification.kind.rejected":`Rejected`,"verification.kind.cancelled":`Cancelled`,"verification.kind.revoked":`Badge revoked`,"verification.kind.notified":`Applicant notified`,"verification.actionDock":`Review actions`,"verification.noActions":`This status has no available actions.`,"verification.claim":`Take into review`,"verification.claimHint":`Assigns the application to you and moves it to in review, so two reviewers never work on the same one.`,"verification.internalNotePlaceholder":`Handover note for other reviewers`,"verification.internalNoteHint":`Optional. Stored with the decision and visible to admins only — never sent to the applicant.`,"verification.alreadyVerifiedHint":`The target already carries the badge; approving only records the decision.`,"verification.approve":`Approve`,"verification.approveHint":`Grants the official badge to the target and closes the application.`,"verification.reject":`Reject`,"verification.rejectHint":`The reason is mandatory: it is the wording the applicant is told, so write what exactly was missing.`,"verification.dangerZone":`Danger zone`,"verification.revoke":`Revoke verification`,"verification.revokeHint":`Clears the badge from the target. The approved application stays in history.`,"verification.revokeNotVerified":`The target carries no badge right now — there is nothing to revoke.`,"route.botVerification":`Third-party verification`,"route.botVerificationSubtitle":`Console / Third-party verification`,"layout.botVerification":`Third-party marks`,"picker.system":`System`,"picker.botPlaceholder":`Bot username or id`,"botverification.pageTitle":`Third-party verification`,"botverification.eyebrow":`Third-party verification / Verifiers, icons, marks`,"botverification.explainTitle":`A verifier company's icon — not the official checkmark`,"botverification.explainText":`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.`,"botverification.explainIcon":`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.`,"botverification.explainOfficial":`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.`,"botverification.openOfficial":`Official verification`,"botverification.manageMissing":`This session can read the section and decide applications, but not change verifiers or the icon catalogue — that needs the botverification.manage permission.`,"botverification.tabRequests":`Applications`,"botverification.tabVerifiers":`Verifiers`,"botverification.tabIcons":`Icon catalogue`,"botverification.tabMarks":`Granted marks`,"botverification.queueTitle":`Application queue`,"botverification.queueHint":`Applications filed with a verifier bot by the owner of the peer. The counters cover the whole queue, not the page below.`,"botverification.searchPlaceholder":`Application id, peer id, username or title`,"botverification.statusAll":`All statuses`,"botverification.status.pending":`Pending`,"botverification.status.approved":`Approved`,"botverification.status.rejected":`Rejected`,"botverification.status.revoked":`Mark revoked`,"botverification.peer.user":`Account`,"botverification.peer.channel":`Channel`,"botverification.peerType":`Peer type`,"botverification.peerTypeAll":`All types`,"botverification.verifier":`Verifier`,"botverification.verifierAll":`All verifiers`,"botverification.verifierID":`Verifier bot ID`,"botverification.applicant":`Applicant`,"botverification.applicantID":`User ID`,"botverification.target":`Peer`,"botverification.targetTitle":`Title`,"botverification.targetID":`Peer ID`,"botverification.reason":`Stated reason`,"botverification.requestedDescription":`Requested description`,"botverification.description":`Description`,"botverification.createdAt":`Filed`,"botverification.company":`Company`,"botverification.companyPlaceholder":`Acme Verification Ltd`,"botverification.bot":`Bot`,"botverification.icon":`Icon`,"botverification.iconDocument":`Document ID`,"botverification.iconName":`Name`,"botverification.markCount":`Marks`,"botverification.grantedBy":`Granted by`,"botverification.notProvided":`Not set`,"botverification.verifiersTitle":`Verifier bots`,"botverification.verifiersHint":`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.`,"botverification.grantTitle":`Grant verifier status`,"botverification.updateTitle":`Update verifier`,"botverification.grantHint":`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.`,"botverification.grantBot":`Bot`,"botverification.grantIcon":`Icon from the catalogue`,"botverification.grantIconPick":`Pick an icon`,"botverification.defaultDescription":`Default description`,"botverification.defaultDescriptionPlaceholder":`Verified by Acme`,"botverification.canModify":`The verifier may replace the description per peer`,"botverification.canModifyShort":`Own description`,"botverification.canModifyHint":`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.`,"botverification.noActiveIcons":`The catalogue has no active icon, so there is nothing to grant. Add one in the icon catalogue first.`,"botverification.grantNote":`The bot can mark peers as soon as the row exists and is enabled.`,"botverification.grant":`Grant verifier status`,"botverification.update":`Update verifier`,"botverification.editing":`Updating {bot} — version {version} is sent as the optimistic lock, so a row somebody else changed meanwhile is refused instead of overwritten.`,"botverification.cancelEdit":`Cancel update`,"botverification.edit":`Edit`,"botverification.enable":`Enable`,"botverification.disable":`Disable`,"botverification.enabled":`Enabled`,"botverification.disabled":`disabled`,"botverification.disableHint":`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.`,"botverification.revokeVerifier":`Revoke status`,"botverification.revokeVerifierHint":`Revoking verifier status removes the row and every mark this verifier granted — the icon disappears from all of its peers at once.`,"botverification.iconsTitle":`Icon catalogue`,"botverification.iconsHint":`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.`,"botverification.addIconTitle":`Add or rename an icon`,"botverification.addIconHint":`The document id has to name a real custom emoji document on this deployment; the Emoji section lists them with their ids. Adding an id that already exists renames it instead of duplicating it.`,"botverification.iconNamePlaceholder":`Acme blue tick`,"botverification.iconOwner":`Owner`,"botverification.iconOwnerShared":`Shared`,"botverification.iconOwnerHint":`A shared icon may be granted to any verifier; picking an owner reserves it for that one bot.`,"botverification.iconDocumentHint":`A document id that resolves to nothing produces an invisible badge: the peer is marked in the database and the client draws nothing.`,"botverification.addIconNote":`Adding an icon grants nothing by itself — it only makes the document available to grant.`,"botverification.addIcon":`Save icon`,"botverification.iconActive":`Active`,"botverification.iconInactive":`Retired`,"botverification.usedBy":`Verifiers using it`,"botverification.activateIcon":`Activate`,"botverification.deactivateIcon":`Retire`,"botverification.deactivateIconHint":`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.`,"botverification.marksTitle":`Granted marks`,"botverification.marksHint":`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.`,"botverification.markSearchPlaceholder":`Peer id, username or title`,"botverification.revokeMark":`Remove mark`,"botverification.revokeMarkHint":`Removing a mark clears the icon and the description from the peer. The application it came from keeps its history.`,"botverification.loadingDetail":`Loading the application…`,"botverification.detailTitle":`Application #{id}`,"botverification.detailEyebrow":`Third-party verification / Review`,"botverification.conflict":`Another admin has already changed this application. The data has been reloaded — check the status before deciding again.`,"botverification.markActive":`Mark is live`,"botverification.markInactive":`No mark on the peer`,"botverification.markActiveHint":`This peer already carries this verifier's mark; approving refreshes the description and records the decision.`,"botverification.verifierSection":`Verifier`,"botverification.verifierHint":`The company whose icon the peer would carry, as its row stands right now.`,"botverification.openVerifier":`Open verifier bot`,"botverification.verifierMissing":`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.`,"botverification.verifierDisabledHint":`This verifier is disabled. It cannot mark anything new until an operator enables it again.`,"botverification.targetSection":`Peer`,"botverification.targetHint":`The account, bot or channel the icon would be attached to.`,"botverification.openTarget":`Open peer`,"botverification.applicantSection":`Applicant`,"botverification.applicantHint":`Who filed the application with the verifier bot.`,"botverification.openApplicant":`Open account`,"botverification.requestSection":`Application`,"botverification.requestHint":`What the applicant wrote, rendered as plain text.`,"botverification.correlationID":`Correlation ID`,"botverification.markPreview":`Description the mark would carry`,"botverification.markPreviewHint":`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.`,"botverification.descriptionIgnoredHint":`This verifier may not set a per-peer description, so the requested wording is ignored and the default is applied.`,"botverification.decisionSection":`Decision`,"botverification.decisionHint":`What was decided, by whom, and with which wording.`,"botverification.decidedBy":`Decided by`,"botverification.approvedAt":`Approved`,"botverification.rejectedAt":`Rejected`,"botverification.version":`Version (optimistic lock)`,"botverification.decisionReason":`Decision reason`,"botverification.noDecision":`No decision yet`,"botverification.internalNote":`Internal note`,"botverification.adminOnly":`admins only`,"botverification.internalNotePlaceholder":`Handover note for other admins`,"botverification.internalNoteHint":`Optional. Stored with the decision and visible to admins only — never sent to the applicant.`,"botverification.actionDock":`Decision`,"botverification.noActions":`This status has no available actions.`,"botverification.approve":`Approve`,"botverification.approveHint":`Puts the verifier's icon before the peer's name and its description in the profile, and messages the applicant.`,"botverification.reject":`Reject`,"botverification.rejectHint":`The reason is mandatory: it is the wording the applicant is told, so write what exactly was missing.`,"botverification.dangerZone":`Danger zone`,"botverification.revokeRequest":`Revoke mark`,"botverification.revokeRequestHint":`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.`,"botverification.revokeNoMark":`The peer carries no mark right now — revoking only closes the application.`,"botverification.rosterDenied":`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.`},zh:{"app.adminConsole":`管理控制台`,"app.localAccess":`本地访问`,"app.title":`telesrv 管理后台`,"common.actions":`操作`,"common.admins":`管理员`,"common.backToList":`返回列表`,"common.channel":`频道`,"common.channelOrGroup":`频道/群`,"common.clear":`清除`,"common.close":`关闭`,"common.count":`数量`,"common.deleted":`已删除`,"common.detail":`详情`,"common.device":`设备`,"common.disabled":`已禁用`,"common.enabled":`已启用`,"common.fromPeer":`From Peer`,"common.group":`群组`,"common.id":`ID`,"common.limit":`条数`,"common.loading":`加载中`,"common.member":`成员`,"common.members":`成员`,"common.messageId":`消息 ID`,"common.name":`姓名`,"common.no":`否`,"common.noResults":`无结果`,"common.none":`无`,"common.normal":`正常`,"common.operations":`操作`,"common.owner":`所属`,"common.platform":`平台`,"common.refresh":`刷新`,"common.search":`查询`,"common.sender":`发送方`,"common.status":`状态`,"common.survived":`存活`,"common.time":`时间`,"common.type":`类型`,"common.updatedAt":`更新时间`,"common.username":`用户名`,"common.valid":`有效`,"common.verified":`已认证`,"common.views":`浏览`,"common.yes":`是`,"route.accounts":`账号管理`,"route.accountsSubtitle":`控制台 / 账号`,"route.channels":`超级群与频道`,"route.channelsSubtitle":`控制台 / 频道`,"route.moderation":`举报与审核`,"route.moderationSubtitle":`控制台 / 内容安全`,"route.dashboard":`运维控制台`,"route.dashboardSubtitle":`控制台 / 总览`,"route.messages":`消息审计`,"route.messagesSubtitle":`控制台 / 消息`,"route.gifts":`星星礼物`,"route.giftsSubtitle":`控制台 / 星星礼物`,"route.giveGifts":`赠送礼物`,"route.giveGiftsSubtitle":`控制台 / 赠送礼物`,"layout.navigation":`导航`,"layout.primaryNav":`主导航`,"layout.dashboard":`总览`,"layout.accounts":`账号`,"layout.channels":`超级群/频道`,"layout.moderation":`举报/审核`,"layout.messages":`消息`,"layout.gifts":`礼物目录`,"layout.giveGifts":`赠送礼物`,"layout.privateMessages":`私聊`,"layout.groupMessages":`群聊`,"layout.runtime":`运行状态`,"layout.adminBackend":`管理后台`,"layout.ready":`就绪`,"layout.pgRead":`PG 读取`,"layout.readOnly":`只读`,"layout.writeOps":`写操作`,"layout.dryRun":`预演`,"layout.actor":`操作者:{actor}`,"layout.logout":`退出`,"language.en":`EN`,"language.zh":`中文`,"language.ru":`RU`,"theme.switchToDark":`切换到深色主题`,"theme.switchToLight":`切换到浅色主题`,"login.heading":`运维后台`,"login.body":`输入凭据后进入控制台。`,"login.secret":`管理员密码或 token`,"login.submit":`登录`,"login.submitting":`登录中`,"dashboard.eyebrow":`运行总览`,"dashboard.title":`控制台总览`,"dashboard.readPath":`读路径`,"dashboard.readPathValue":`PG 只读`,"dashboard.writePath":`写路径`,"dashboard.executionPolicy":`执行策略`,"dashboard.dryRunFirst":`先预演`,"dashboard.accountsText":`账号状态、会员、认证、会话。`,"dashboard.channelsText":`公开实体、成员计数、认证状态。`,"dashboard.messagesText":`消息盒、update、outbox 状态。`,"dashboard.strip.dryRun":`所有危险操作先预演`,"dashboard.strip.token":`浏览器不持有内部 token`,"dashboard.strip.pagination":`列表使用游标分页`,"dashboard.strip.snapshot":`详情页保留原始状态快照`,"account.pageTitle":`账号`,"account.queryResults":`查询结果`,"account.recentActive":`最近活跃账号`,"account.currentPage":`当前页账号`,"account.onlineDevices":`在线设备记录`,"account.premium":`会员`,"account.frozen":`冻结`,"account.searchPlaceholder":`用户 ID / 手机号 / 用户名`,"account.userID":`用户 ID`,"account.phone":`手机号`,"account.lastActive":`最近活跃`,"account.notVerified":`未认证`,"account.notPremium":`非会员`,"account.premiumUntil":`会员到期`,"account.starsBalance":`Stars 余额`,"account.startingGrantApplied":`初始赠送已发放`,"account.startingGrantPending":`初始赠送未触发`,"account.activeSessions":`授权设备`,"account.accountFlags":`账号标记`,"account.restriction":`限制状态`,"account.restricted":`已限制`,"account.createdAt":`创建时间`,"account.detailTitle":`账号 #{id}`,"account.profile":`账号档案`,"account.loadingDetail":`加载账号详情`,"account.waitingData":`等待数据`,"account.noUsername":`无用户名`,"account.noPhone":`无手机号`,"account.accountFrozen":`账号已冻结`,"account.accountActive":`账号正常`,"account.authorizationsTitle":`授权设备`,"account.authorizationsCount":`共 {count} 个授权`,"account.recentAdminOps":`最近后台操作`,"account.recent30Audit":`最近 30 条审计`,"account.actionDock":`账号操作`,"account.freezeAccount":`冻结账号`,"account.updateFreeze":`更新冻结信息`,"account.unfreezeAccount":`解冻账号`,"account.freezeSince":`冻结开始时间`,"account.freezeUntil":`申诉截止时间`,"account.freezeUntilAria":`账号冻结申诉截止时间`,"account.freezeAppealURL":`申诉链接`,"account.freezeAppealURLAria":`账号冻结申诉链接`,"account.premiumMonths":`会员时长(月)`,"account.premiumMonthsAria":`设置会员时长,单位月`,"account.setPremium":`设置会员`,"account.clearPremium":`取消会员`,"account.starsAmount":`赠送 Stars 数量`,"account.starsAmountAria":`设置要赠送的 Stars 数量`,"account.grantStars":`赠送 Stars`,"account.setVerified":`设置认证`,"account.clearVerified":`取消认证`,"channel.pageTitle":`超级群与频道`,"channel.recentUpdated":`最近更新`,"channel.currentPage":`当前页实体`,"channel.megagroups":`超级群`,"channel.broadcasts":`频道`,"channel.verifiedCount":`已认证`,"channel.searchPlaceholder":`频道 ID / 用户名 / 标题`,"channel.channelID":`频道 ID`,"channel.kind":`类型`,"channel.title":`标题`,"channel.pts":`PTS`,"channel.detailProfile":`频道档案`,"channel.loadingDetail":`加载频道详情`,"channel.creator":`创建者 {id}`,"channel.governance":`治理状态`,"channel.governanceValue":`封禁 {banned} / 踢出 {kicked}`,"channel.flags":`频道标记`,"channel.rawRow":`频道原始行`,"channel.rawRowText":`数据库只读快照`,"channel.actionDock":`频道操作`,"channel.setVerified":`设置认证`,"channel.clearVerified":`取消认证`,"channel.kind.broadcast":`频道`,"channel.kind.forum":`超级群/论坛`,"channel.kind.megagroup":`超级群`,"channel.kind.generic":`频道/群`,"route.bots":`机器人`,"route.botsSubtitle":`控制台 / 机器人`,"layout.bots":`机器人`,"bots.pageTitle":`机器人`,"bots.queryResults":`查询结果`,"bots.recent":`最近创建的机器人`,"bots.currentPage":`当前页机器人`,"bots.banned":`已封禁`,"bots.active":`正常`,"bots.createTitle":`创建系统机器人`,"bots.createHint":`为指定用户创建机器人账号。确认后 token 只显示一次。`,"bots.ownerUserID":`所属用户 ID`,"bots.name":`显示名称`,"bots.namePlaceholder":`例如:服务机器人`,"bots.username":`用户名`,"bots.usernameHint":`用户名需 5-32 个字符,且以 bot 结尾。`,"bots.create":`创建机器人`,"bots.searchPlaceholder":`机器人 ID / 用户名`,"bots.botID":`机器人 ID`,"bots.owner":`所属用户`,"bots.status":`状态`,"bots.detailTitle":`机器人 #{id}`,"bots.profile":`机器人档案`,"bots.loadingDetail":`加载机器人详情`,"bots.unnamed":`未命名机器人`,"bots.restriction":`限制状态`,"bots.actionDock":`机器人操作`,"bots.banUntil":`封禁至`,"bots.ban":`封禁机器人`,"bots.updateBan":`更新封禁`,"bots.unban":`解封机器人`,"bots.type":`类型`,"bots.system":`系统`,"bots.user":`用户`,"bots.delete":`删除机器人`,"bots.deleteHint":`永久删除该用户创建的机器人并使其 token 失效。此操作不可撤销。`,"bots.systemHint":`系统内置机器人不可删除。`,"flags.scam":`SCAM`,"flags.fake":`FAKE`,"flags.setScam":`标记为 SCAM`,"flags.clearScam":`移除 SCAM`,"flags.setFake":`标记为 FAKE`,"flags.clearFake":`移除 FAKE`,"attr.attributes":`属性`,"attr.settings":`设置`,"attr.username":`用户名`,"attr.setUsername":`设置用户名`,"attr.setSupport":`标记为客服`,"attr.clearSupport":`取消客服`,"attr.forProfile":`资料颜色`,"attr.hasColor":`启用颜色`,"attr.colorIndex":`颜色编号`,"attr.bgEmojiID":`背景 emoji ID`,"attr.setColor":`设置颜色`,"attr.emojiDocID":`Emoji 文档 ID`,"attr.emojiUntil":`有效期 (unix, 0 = 永久)`,"attr.setEmojiStatus":`设置 emoji 状态`,"attr.gigagroup":`广播群 (gigagroup)`,"attr.antispam":`激进反垃圾`,"attr.participantsHidden":`隐藏成员`,"attr.noforwards":`禁止转发`,"attr.joinToSend":`先加入才能发言`,"attr.joinRequest":`加入需审批`,"attr.slowmode":`慢速模式 (秒)`,"attr.applySettings":`应用设置`,"route.emoji":`Emoji`,"route.emojiSubtitle":`控制台 / Emoji`,"layout.emoji":`Emoji`,"emoji.pageTitle":`自定义 Emoji`,"emoji.queryResults":`查询结果`,"emoji.recent":`自定义 Emoji 目录`,"emoji.currentPage":`当前页 Emoji`,"emoji.searchPlaceholder":`文档 ID 或表情`,"emoji.copyID":`复制文档 ID`,"emoji.noSet":`无所属集合`,"emoji.hint":`这里的文档 ID 可直接填入账号、机器人和频道资料的 Emoji 状态字段。`,"messages.privateTitle":`私聊消息`,"messages.privateEyebrow":`私聊消息盒`,"messages.groupTitle":`群聊消息`,"messages.groupEyebrow":`超级群 / 频道消息`,"messages.selectPrivatePeers":`请先搜索并选择所属用户和对端用户`,"messages.selectChannel":`请先搜索并选择超级群或频道`,"messages.ownerUser":`所属用户`,"messages.peerUser":`对端用户`,"messages.beforeDatePlaceholder":`before_date 游标`,"messages.beforeIDPlaceholder":`before_msg_id 游标`,"messages.limitPlaceholder":`条数 <= 100`,"messages.searchMessages":`查询消息`,"messages.nextPage":`下一页`,"messages.currentPage":`当前页消息`,"messages.deleted":`已删除`,"messages.outgoing":`发出消息`,"messages.incoming":`收到`,"messages.ownerPeer":`所属 / 对端`,"messages.deleteSelected":`删除指定消息`,"messages.idsPlaceholder":`消息 ID,逗号分隔`,"messages.revoke":`同步撤回`,"messages.previewDelete":`预演删除`,"messages.clearHistory":`清空私聊历史`,"messages.maxIDPlaceholder":`max_id 截止消息`,"messages.maxBatchesPlaceholder":`max_batches 批次数`,"messages.justClear":`仅清本侧`,"messages.previewClearHistory":`预演清历史`,"messages.direction":`方向`,"messages.body":`正文`,"messages.privateDetailTitle":`消息 #{id}`,"messages.detailEyebrow":`消息详情`,"messages.backPrivate":`返回私聊消息`,"messages.backGroup":`返回群聊消息`,"messages.ownerPeerTitle":`所属 {owner} · 对端 {peer}`,"messages.senderSubtitle":`发送方 {sender} · {date}`,"messages.boxID":`消息盒 ID`,"messages.privateMessageID":`私聊消息 ID`,"messages.messageSender":`发送方`,"messages.messageBox":`消息盒`,"messages.dialogRow":`会话行`,"messages.privateRow":`私聊消息行`,"messages.channelMessageRow":`消息行`,"messages.channelRow":`频道行`,"messages.userUpdateEvents":`更新事件`,"messages.channelUpdateEvents":`频道更新事件`,"messages.eventJson":`事件 JSON`,"messages.dispatchOutbox":`分发队列`,"messages.messageBoxesSnapshot":`message_boxes 只读快照`,"messages.dialogSnapshot":`dialogs 只读快照`,"messages.privateSnapshot":`private_messages 只读快照`,"messages.channelMessagesSnapshot":`channel_messages 只读快照`,"messages.channelSnapshot":`channels 只读快照`,"messages.userEventsSource":`durable user_update_events`,"messages.channelEventsSource":`durable channel_update_events`,"messages.outboxSource":`在线/离线 dispatch_outbox`,"messages.attempts":`尝试`,"messages.deleteThis":`删除此消息`,"messages.groupDetailTitle":`群聊消息 #{id}`,"messages.channelGroupTitle":`频道/群 {id}`,"messages.mediaCount":`有媒体`,"messages.channelPosts":`频道帖子`,"messages.channelGroup":`频道 / 群`,"messages.pinned":`置顶`,"messages.channelPost":`频道帖子`,"gifts.pageTitle":`星星礼物目录`,"giveGift.action":`赠送`,"giveGift.eyebrow":`发放礼物 · 免费`,"giveGift.title":`赠送礼物`,"giveGift.recipientKind":`接收方类型`,"giveGift.recipientUser":`用户`,"giveGift.recipientChannel":`频道`,"giveGift.pickUser":`接收用户`,"giveGift.pickChannel":`接收频道`,"giveGift.recipientRequired":`请先选择接收方`,"giveGift.sender":`发送方账号 ID`,"giveGift.senderHint":`礼物始终由系统账号 777000(Telesrv)发送。`,"giveGift.message":`附加留言(可选)`,"giveGift.messagePlaceholder":`随礼物一起显示`,"giveGift.hideName":`对接收方隐藏发送方名称`,"giveGift.upgrade":`作为升级收藏品发放`,"giveGift.upgradeNote":`礼物将铸造为唯一收藏品。可在下方指定具体属性,或保持“随机”从已发布的属性池中抽取。编号自动分配。需要存在有剩余供应量的已发布收藏品升级。`,"giveGift.model":`模型`,"giveGift.pattern":`图案`,"giveGift.backdrop":`背景`,"giveGift.random":`随机`,"giveGift.confirm":`赠送礼物`,"giveGifts.pageTitle":`赠送礼物`,"giveGifts.eyebrow":`向任意用户或频道发放目录礼物`,"giveGifts.available":`可用礼物`,"giveGifts.sender":`默认发送方`,"giveGifts.searchPlaceholder":`按标题或礼物 ID 搜索`,"giveGifts.hint":`选择要发放的礼物。发放免费,默认由系统账号 777000(Telesrv)发送。`,"giveGifts.pickGift":`选择礼物`,"giveGifts.selectPrompt":`从列表中选择一个礼物开始。`,"gifts.eyebrow":`目录、不可变版本与动画资源`,"gifts.total":`目录条目`,"gifts.enabled":`已启用`,"gifts.received":`已领取礼物`,"gifts.formats":`支持格式`,"gifts.add":`添加礼物`,"gifts.searchPlaceholder":`搜索礼物 ID、标题或格式`,"gifts.listSummary":`显示 {shown} / {total} 项`,"gifts.idRevision":`ID / 版本`,"gifts.price":`售价 / 兑换`,"gifts.importTitle":`导入星星礼物`,"gifts.importEyebrow":`礼物目录操作`,"gifts.newRevision":`为礼物 #{id} 创建新版本`,"gifts.importHint":`支持 TGS 或纯 Lottie JSON;Lottie 会规范化并压缩成 TGS。`,"gifts.officialSource":`官方资源库`,"gifts.fileSource":`上传文件`,"gifts.officialHint":`从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。`,"gifts.officialSearch":`搜索官方礼物 ID 或标题`,"gifts.officialSelect":`请选择官方礼物`,"gifts.officialRequired":`请先选择一个官方礼物`,"gifts.officialResults":`显示 {shown} / {total} 项`,"gifts.officialCategoryLabel":`官方礼物能力分类`,"gifts.officialCategory.all":`全部`,"gifts.officialCategory.upgrade":`可升级`,"gifts.officialCategory.craft":`可 Craft`,"gifts.officialCategory.basic":`不可升级`,"gifts.officialUnnamed":`未命名官方礼物 #{id}`,"gifts.officialAttributes":`{count} 个属性`,"gifts.canUpgrade":`可升级`,"gifts.cannotUpgrade":`不可升级`,"gifts.canCraft":`可 Craft`,"gifts.cannotCraft":`不可 Craft`,"gifts.officialEmpty":`当前分类和搜索条件下没有官方礼物。`,"gifts.includeCollectible":`完整导入 collectible 属性池(包含 crafted 模型)`,"gifts.animation":`动画文件`,"gifts.filePrompt":`拖放或选择 TGS / Lottie 文件`,"gifts.fileHint":`支持 TGS、JSON、Lottie,导入前会先进行校验`,"gifts.chooseFile":`选择文件`,"gifts.changeFile":`更换文件`,"gifts.title":`显示标题`,"gifts.titlePlaceholder":`例如:庆典星星`,"gifts.stars":`售价 Stars`,"gifts.convertStars":`可兑换 Stars`,"gifts.sortOrder":`排序值`,"gifts.reason":`审计原因`,"gifts.reasonPlaceholder":`简要说明本次导入礼物的原因`,"gifts.enableAfterImport":`导入后启用`,"gifts.validate":`Dry-run 校验`,"gifts.confirmImport":`确认导入`,"gifts.stepDetails":`文件与信息`,"gifts.stepValidate":`Dry-run 校验`,"gifts.stepImport":`确认导入`,"gifts.fileRequired":`请先选择 TGS 或 Lottie 文件`,"gifts.source":`来源`,"gifts.replace":`创建新版本`,"gifts.disable":`停用`,"gifts.enable":`启用`,"gifts.empty":`尚未导入星星礼物。`,"gifts.emptyHint":`从上方导入第一个动画,开始搭建礼物目录。`,"gifts.validationReady":`校验已通过`,"gifts.validationHint":`确认规范化后的元数据无误,再执行正式导入。`,"gifts.confirmState":`确认执行礼物 #{id} 的状态变更吗?`,"collectibles.manage":`属性池`,"collectibles.title":`Collectibles 属性池 · 礼物 #{id}`,"collectibles.eyebrow":`唯一礼物属性管理`,"collectibles.activeRevision":`已发布版本 {revision}`,"collectibles.published":`已发布`,"collectibles.noPool":`尚未发布 Collectibles 属性池`,"collectibles.noPoolHint":`发布模型、图案与背景后,客户端即可升级为唯一礼物。`,"collectibles.publishNew":`发布新的不可变版本`,"collectibles.immutableHint":`Dry-run 会校验全部文件和属性池结构,通过后才切换为当前版本。`,"collectibles.upgradeStars":`升级价格 Stars`,"collectibles.supply":`唯一礼物总量`,"collectibles.slug":`公开 Slug 前缀`,"collectibles.models":`模型`,"collectibles.patterns":`图案`,"collectibles.backdrops":`背景`,"collectibles.model":`模型`,"collectibles.pattern":`图案`,"collectibles.backdrop":`背景`,"collectibles.rarity":`稀有度 ‰`,"collectibles.rarityHint":`每类至少保留两项。Permille 是普通升级的相对权重,增删时会自动重新分配为 1000。`,"collectibles.minimumAttributes":`模型、图案和背景每类都必须至少包含两个属性。`,"collectibles.duplicateBackdropID":`同一属性池中的背景 ID 必须唯一。`,"collectibles.colorHint":`颜色会按 24 位 RGB 数值保存。`,"collectibles.addAttribute":`添加`,"collectibles.remove":`删除属性`,"collectibles.fileRequired":`每个模型和图案都必须选择 TGS 或 Lottie 文件。`,"collectibles.backdropID":`背景 ID`,"collectibles.color.center":`中心色`,"collectibles.color.edge":`边缘色`,"collectibles.color.pattern":`图案色`,"collectibles.color.text":`文字色`,"collectibles.validationReady":`属性池校验通过`,"collectibles.validationHint":`确认规范化资源无误后,即可发布这个不可变版本。`,"collectibles.publish":`发布版本`,"moderation.casesEyebrow":`内容安全 / 案件`,"moderation.currentQueue":`当前队列`,"moderation.criticalCases":`关键案件`,"moderation.pendingOrFailed":`处置待完成 / 失败`,"moderation.statusFilter":`案件状态筛选`,"moderation.statusFilter.active":`活跃队列`,"moderation.statusFilter.all":`全部状态`,"moderation.assignee":`审核人`,"moderation.allAssignees":`留空为全部`,"moderation.case":`案件`,"moderation.target":`目标`,"moderation.severity":`等级`,"moderation.reportsAndReporters":`举报 / 举报人`,"moderation.latestReport":`最近举报`,"moderation.review":`审核`,"moderation.status.open":`待审核`,"moderation.status.in_review":`审核中`,"moderation.status.action_pending":`等待处置`,"moderation.status.action_failed":`处置失败`,"moderation.status.resolved":`已处置`,"moderation.status.dismissed":`已驳回`,"moderation.status.appeal_review":`申诉复核中`,"moderation.severity.low":`低`,"moderation.severity.medium":`中`,"moderation.severity.high":`高`,"moderation.severity.critical":`关键`,"moderation.targetType.user":`账号`,"moderation.targetType.chat":`群组`,"moderation.targetType.channel":`频道`,"moderation.caseDetailTitle":`审核案件 #{id}`,"moderation.caseDetailEyebrow":`内容安全 / 案件详情`,"moderation.backToQueue":`返回队列`,"moderation.loadingCase":`正在加载审核案件…`,"moderation.versionAndUpdated":`版本 {version} · 最近更新 {time}`,"moderation.reportCount":`举报数`,"moderation.reportCountValue":`{reports} 次({reporters} 位举报人)`,"moderation.firstAndLatestReport":`首个 / 最近举报`,"moderation.evidence":`举报证据`,"moderation.evidenceHint":`最多显示最近 100 条;快照在举报受理时冻结。`,"moderation.sourceAndReason":`来源 / 原因`,"moderation.reporter":`举报人`,"moderation.option":`选项`,"moderation.decisionAudit":`决定与处置审计`,"moderation.decisionAuditHint":`动作由租约 worker 幂等执行;失败保留错误与尝试次数。`,"moderation.appeals":`申诉`,"moderation.caseActions":`案件操作`,"moderation.renewClaim":`续领案件`,"moderation.claimCase":`领取案件`,"moderation.reviewReason":`审核理由`,"moderation.decisionPreset":`决定模板`,"moderation.preset.noViolation":`无违规(驳回举报)`,"moderation.preset.scam":`标记 SCAM`,"moderation.preset.fake":`标记 FAKE`,"moderation.preset.freeze":`冻结账号`,"moderation.preset.scamFreeze":`SCAM + 冻结`,"moderation.preset.fakeFreeze":`FAKE + 冻结`,"moderation.preset.deleteMessages":`删除证据覆盖的消息`,"moderation.preset.deleteAccount":`删除账号`,"moderation.evidenceMessageIDs":`证据消息 ID(逗号分隔)`,"moderation.privateOwnerUserID":`私聊 owner_user_id`,"moderation.revokeForBoth":`双方撤回`,"moderation.evidenceValidationHint":`服务端会再次校验每个消息 ID 必须存在于该案件的不可变举报证据中。`,"moderation.failedActionHint":`处置已部分执行,不能直接改为无违规;请选择新的处置动作重新执行并保留旧失败审计。`,"moderation.retryAction":`重新执行处置`,"moderation.submitDecision":`提交决定`,"moderation.appealReviewTitle":`申诉复核 #{id}`,"moderation.automaticRemedy":`通过后自动恢复`,"moderation.irreversibleAppealHint":`案件包含已成功的不可逆删除动作,不能标记为“申诉通过并已恢复”;请驳回或升级人工处理。`,"moderation.denyAppeal":`驳回申诉`,"moderation.grantAppeal":`通过申诉`,"moderation.reasonRequired":`必须填写审核理由。`,"moderation.appealReasonRequired":`必须填写申诉复核理由。`,"moderation.privateDeleteValidation":`私聊删除需要合法的证据消息 ID 和举报人 owner_user_id。`,"moderation.channelDeleteValidation":`频道删除需要至少一个合法的证据消息 ID。`,"moderation.confirmDecision":`确认提交“{decision}”决定?处置会通过 durable action 队列执行。`,"moderation.confirmGrantAppeal":`确认通过申诉?`,"moderation.confirmDenyAppeal":`确认驳回申诉?`,"moderation.remedy.clearFlags":`清除 SCAM / FAKE`,"moderation.remedy.unfreeze":`解除冻结`,"moderation.remedy.none":`无需恢复动作`,"moderation.source.account_peer":`账号 / Peer`,"moderation.source.profile_photo":`资料照片`,"moderation.source.messages_spam":`消息垃圾内容`,"moderation.source.messages":`消息`,"moderation.source.encrypted_spam":`加密聊天垃圾内容`,"moderation.source.reaction":`回应`,"moderation.source.channel_spam":`频道垃圾内容`,"moderation.source.story":`Story`,"moderation.source.ephemeral":`阅后即焚媒体`,"moderation.source.sponsored":`赞助消息`,"moderation.source.antispam_false_positive":`反垃圾误判`,"moderation.reason.spam":`垃圾内容`,"moderation.reason.violence":`暴力`,"moderation.reason.pornography":`色情内容`,"moderation.reason.child_abuse":`儿童虐待`,"moderation.reason.other":`其他`,"moderation.reason.copyright":`版权`,"moderation.reason.geo_irrelevant":`与地区无关`,"moderation.reason.fake":`虚假信息`,"moderation.reason.illegal_drugs":`非法药物`,"moderation.reason.personal_details":`个人信息`,"messages.msgIDsInvalid":`消息 ID 无效`,"auth.device":`设备`,"auth.platform":`平台`,"auth.ip":`IP`,"auth.lastActive":`最近活跃`,"auth.revokeCurrent":`撤销当前`,"auth.keepCurrent":`保留当前`,"auth.revokeAll":`撤销全部设备`,"picker.userPlaceholder":`搜索 user_id / phone / username`,"picker.channelPlaceholder":`搜索 channel_id / username / title`,"picker.verified":`认证`,"picker.regular":`普通`,"action.reasonRequired":`请填写操作原因`,"action.flow":`操作流程`,"action.close":`关闭`,"action.stepReason":`填写原因`,"action.stepDryRun":`预演检查`,"action.stepConfirm":`确认执行`,"action.reason":`操作原因`,"action.reasonPlaceholder":`说明本次操作原因`,"action.requestPreview":`请求预览`,"action.result":`操作结果`,"action.commandID":`命令 ID`,"action.status":`状态`,"action.dryRun":`预演`,"action.runAgain":`重新预演`,"action.runDry":`先预演`,"action.confirm":`确认执行`,"audit.id":`ID`,"audit.commandID":`命令 ID`,"audit.action":`动作`,"audit.actor":`操作者`,"audit.status":`状态`,"audit.dryRun":`预演`,"audit.reason":`原因`,"audit.time":`时间`,"common.loadMore":`加载更多`,"route.collectibleUsernames":`收藏用户名`,"route.collectibleUsernamesSubtitle":`控制台 / 收藏用户名`,"route.accountRatings":`账号评级`,"route.accountRatingsSubtitle":`控制台 / 账号评级`,"layout.collectibleUsernames":`NFT 用户名`,"layout.accountRatings":`账号评级`,"usernames.pageTitle":`收藏用户名`,"usernames.eyebrow":`NFT 用户名 / 资产台账`,"usernames.metricLoaded":`已加载`,"usernames.metricVault":`库存中`,"usernames.metricOwned":`已归属`,"usernames.metricBurned":`已销毁`,"usernames.mintTitle":`铸造收藏用户名`,"usernames.mintHint":`同时写入购买记录;保持“库存”即不指定归属。`,"usernames.mint":`铸造用户名`,"usernames.mintNote":`用户名、币种与金额必填;预演会先校验可用性。`,"usernames.ownerKind":`归属类型`,"usernames.ownerVault":`库存(无归属)`,"usernames.ownerUser":`归属用户`,"usernames.ownerChannel":`归属频道`,"usernames.currency":`币种`,"usernames.amount":`金额({currency})`,"usernames.cryptoCurrency":`加密币种`,"usernames.cryptoNone":`无`,"usernames.cryptoAmount":`加密金额({currency})`,"usernames.amountHint":`金额按完整的 {currency} 输入,保存时会转换为 API 与 fragment.collectibleInfo 使用的最小单位,客户端才会显示你想要的价格。最多 {decimals} 位小数。客户端将显示:{preview}。`,"usernames.amountInvalid":`这不是有效的 {currency} 金额:只能是数字,最多 {decimals} 位小数。`,"usernames.inactive":`未启用`,"usernames.url":`市场链接`,"usernames.purchaseDate":`购买日期(UTC)`,"usernames.purchaseTime":`购买时间(UTC)`,"usernames.searchPlaceholder":`按用户名搜索`,"usernames.statusAll":`全部状态`,"usernames.statusVault":`库存`,"usernames.statusOwned":`已归属`,"usernames.statusBurned":`已销毁`,"usernames.price":`价格`,"usernames.transfers":`转移次数`,"usernames.registryActive":`资料中展示`,"usernames.registryHidden":`资料中隐藏`,"usernames.loadingDetail":`正在加载收藏用户名…`,"usernames.detailTitle":`收藏用户名 {username}`,"usernames.detailEyebrow":`NFT 用户名 / 资产详情`,"usernames.assetID":`资产 #{id}`,"usernames.transferCount":`转移 {count} 次`,"usernames.originalOwner":`最初归属`,"usernames.openOwnerAccount":`打开归属账号`,"usernames.openOwnerChannel":`打开归属频道`,"usernames.openMarketplace":`打开市场页面`,"usernames.transferTitle":`转移归属`,"usernames.transferHint":`选择接收方;转移会记入流转历史。`,"usernames.recipientKind":`接收方类型`,"usernames.recipientUser":`转给用户`,"usernames.recipientChannel":`转给频道`,"usernames.transferNote":`确认后当前持有者立即失去该用户名。`,"usernames.transfer":`转移`,"usernames.historyTitle":`流转历史`,"usernames.historyHint":`按时间顶序展示铸造、转移、收回与销毁事件。`,"usernames.eventKind":`事件`,"usernames.fromPeer":`来自`,"usernames.toPeer":`转至`,"usernames.actionDock":`资产操作`,"usernames.revoke":`收回至库存`,"usernames.revokeHint":`从持有者收回用户名并放回库存,之后可再次发放。`,"usernames.burn":`永久销毁`,"usernames.burnHint":`不可撤销:用户名将被销毁且永不可再发放。`,"usernames.burnedHint":`该用户名已销毁,无法再执行任何操作。`,"usernames.delete":`删除记录`,"usernames.deleteHint":`彻底删除该藏品及其持有历史,并释放用户名以便重新发放。适用于误发的用户名;销毁则会保留历史。`,"usernames.kind.mint":`铸造`,"usernames.kind.transfer":`转移`,"usernames.kind.revoke":`收回`,"usernames.kind.burn":`销毁`,"rating.pageTitle":`账号评级榜`,"rating.eyebrow":`评级 / 排行榜`,"rating.metricLoaded":`已加载`,"rating.metricTopLevel":`最高等级`,"rating.metricAvgLevel":`平均等级`,"rating.metricPending":`有待生效分值`,"rating.searchPlaceholder":`按用户名、姓名或用户 ID 搜索`,"rating.minLevel":`最低等级`,"rating.userID":`用户 ID`,"rating.level":`等级`,"rating.stars":`分值`,"rating.progress":`升级进度`,"rating.pending":`待生效`,"rating.computedAt":`计算时间`,"rating.levelValue":`{level} 级`,"rating.maxLevel":`已达最高等级`,"rating.progressHint":`距 {target} 还差 {remaining}`,"rating.loadingDetail":`正在加载账号评级…`,"rating.detailTitle":`{user} 的评级`,"rating.detailEyebrow":`评级 / 构成明细`,"rating.pendingBadge":`待生效 {amount}`,"rating.nextLevel":`下一级门槛`,"rating.toNextLevel":`升级所需分值`,"rating.breakdownTitle":`评级构成`,"rating.breakdownHint":`当前分值的来源:星星、活跃度、审核处罚与人工修正。`,"rating.breakdownMismatch":`各项合计为 {sum},而存储分值为 {total};请重新计算以消除偏差。`,"rating.breakdownPending":`各项已包含 {amount},但要到下列日期才计入分值。`,"rating.currentLevelStars":`当前等级门槛`,"rating.nextLevelStars":`下一等级门槛`,"rating.pendingTitle":`待生效分值`,"rating.pendingHint":`已获得但要到下列日期才计入评级的分值。`,"rating.pendingDate":`生效日期`,"rating.eventsTitle":`评级事件`,"rating.eventsHint":`每一次分值变动及其来源、操作者与原因。`,"rating.eventKind":`来源`,"rating.amount":`变动值`,"rating.actionDock":`评级操作`,"rating.openAccount":`打开账号`,"rating.recompute":`重新计算`,"rating.recomputeHint":`根据星星、活跃度、处罚与人工修正重新生成分值。`,"rating.adjustTitle":`人工修正`,"rating.adjustAmount":`数值(可为负)`,"rating.adjust":`应用修正`,"rating.adjustHint":`该数值会累加到人工修正项;填负数即为扣减。`,"rating.componentStars":`星星`,"rating.componentStarsHint":`购买与收到的星星`,"rating.componentActivity":`活跃度`,"rating.componentActivityHint":`消息、会话与长期活跃`,"rating.componentPenalty":`处罚`,"rating.componentPenaltyHint":`审核处置与限制`,"rating.componentManual":`人工修正`,"rating.componentManualHint":`管理员手动调整`,"rating.componentTotal":`总分`,"rating.kind.stars":`星星`,"rating.kind.activity":`活跃度`,"rating.kind.moderation":`审核`,"rating.kind.manual":`人工`,"rating.kind.recompute":`重新计算`,"route.verification":`官方认证`,"route.verificationSubtitle":`控制台 / 官方认证`,"layout.verification":`官方认证`,"permission.deniedTitle":`权限不足`,"permission.deniedEyebrow":`控制台 / 访问控制`,"permission.deniedBody":`当前会话没有 {permission} 权限,该板块保持关闭。`,"permission.deniedHeading":`板块不可用`,"permission.deniedHint":`请让运维在 TELESRV_ADMIN_UI_PERMISSIONS 中补上该权限,然后重新登录。`,"verification.pageTitle":`认证申请队列`,"verification.eyebrow":`认证 / 队列`,"verification.searchPlaceholder":`申请 ID、对象 ID、用户名或名称`,"verification.statusAll":`全部状态`,"verification.targetType":`对象类型`,"verification.targetTypeAll":`全部类型`,"verification.reviewer":`审核人`,"verification.reviewerPlaceholder":`全部审核人`,"verification.target":`认证对象`,"verification.applicant":`申请人`,"verification.category":`类别`,"verification.submittedAt":`提交时间`,"verification.alreadyVerified":`已有认证标记`,"verification.status.draft":`草稿`,"verification.status.submitted":`已提交`,"verification.status.in_review":`审核中`,"verification.status.approved":`已通过`,"verification.status.rejected":`已驳回`,"verification.status.cancelled":`已取消`,"verification.type.bot":`机器人`,"verification.type.channel":`频道`,"verification.type.supergroup":`超级群`,"verification.type.user":`用户`,"verification.loadingDetail":`正在加载申请…`,"verification.detailTitle":`申请 #{id}`,"verification.detailEyebrow":`认证 / 审核`,"verification.conflict":`该申请已被其他管理员修改,数据已重新加载;请确认状态后再次提交决定。`,"verification.controlsOk":`对象权限已确认`,"verification.controlsLost":`已失去对象权限`,"verification.controlsOkHint":`按当前实时记录核对(而非提交时的快照),申请人此刻仍然掌控该对象。`,"verification.controlsLostHint":`申请人已不再掌控该对象。此时通过,等于把认证标记发给并非持有人的一方,通常应当驳回。`,"verification.targetSection":`认证对象`,"verification.targetHint":`将要挂上认证标记的对象,展示的是当前实时状态。`,"verification.openTarget":`打开对象`,"verification.targetTitle":`名称`,"verification.targetID":`对象 ID`,"verification.applicantSection":`申请人`,"verification.applicantHint":`谁提交了申请,以及他是否仍持有该对象的权限。`,"verification.openApplicant":`打开账号`,"verification.applicantID":`用户 ID`,"verification.applicationSection":`申请内容`,"verification.applicationHint":`申请人填写的全部内容,一律按纯文本展示。`,"verification.correlationID":`关联 ID`,"verification.createdAt":`创建时间`,"verification.description":`说明`,"verification.officialWebsite":`官方网站`,"verification.socialLinks":`社交账号`,"verification.pressLinks":`媒体报道`,"verification.additionalNote":`申请人备注`,"verification.notProvided":`未填写`,"verification.linkSafetyHint":`只有 http:// 与 https:// 链接可点击并在新标签页打开,其余一律按文本显示。`,"verification.decisionSection":`决定`,"verification.decisionHint":`已记录的结论、审核人与理由。`,"verification.reviewedAt":`决定时间`,"verification.version":`版本(乐观锁)`,"verification.decisionReason":`决定理由`,"verification.noDecision":`尚无决定`,"verification.internalNote":`内部备注`,"verification.adminOnly":`仅管理员可见`,"verification.eventsSection":`历史记录`,"verification.eventsHint":`不可篡改的状态流转记录,含操作者与理由。`,"verification.eventKind":`事件`,"verification.transition":`状态变化`,"verification.eventNote":`内部备注`,"verification.kind.created":`创建`,"verification.kind.updated":`修改`,"verification.kind.submitted":`提交`,"verification.kind.claimed":`领取`,"verification.kind.approved":`通过`,"verification.kind.rejected":`驳回`,"verification.kind.cancelled":`取消`,"verification.kind.revoked":`撤销标记`,"verification.kind.notified":`已通知申请人`,"verification.actionDock":`审核操作`,"verification.noActions":`当前状态没有可执行的操作。`,"verification.claim":`领取审核`,"verification.claimHint":`把申请分配给自己并转入审核中,避免两名审核人同时处理同一条。`,"verification.internalNotePlaceholder":`给其他审核人的交接说明`,"verification.internalNoteHint":`可选。随决定一起保存,仅管理员可见,不会发送给申请人。`,"verification.alreadyVerifiedHint":`该对象已带有认证标记,通过操作只是补记这次决定。`,"verification.approve":`通过认证`,"verification.approveHint":`为该对象授予官方认证标记并结束申请。`,"verification.reject":`驳回`,"verification.rejectHint":`必须填写理由:这段文字会告知申请人,请写清究竟缺少什么。`,"verification.dangerZone":`高危操作`,"verification.revoke":`撤销认证`,"verification.revokeHint":`清除该对象的认证标记;已通过的申请仍作为历史保留。`,"verification.revokeNotVerified":`该对象当前没有认证标记,无需撤销。`,"route.botVerification":`第三方认证`,"route.botVerificationSubtitle":`控制台 / 第三方认证`,"layout.botVerification":`第三方标记`,"picker.system":`系统`,"picker.botPlaceholder":`机器人用户名或 ID`,"botverification.pageTitle":`第三方认证`,"botverification.eyebrow":`第三方认证 / 认证方、图标、标记`,"botverification.explainTitle":`这是认证公司的图标,不是官方认证标记`,"botverification.explainText":`第三方标记是认证机器人自己的图标,显示在账号、机器人或频道名称的“前面”,并在资料页附一行说明。它只表示“该认证方为此对象背书”,仅此而已。`,"botverification.explainIcon":`图标是一个自定义表情文档。客户端通过 messages.getCustomEmojiDocuments 拉取,所以指向不存在文档的 ID 会显示为完全没有标记——因此标记只能从下面的图标目录中授予,而不是手输一个数字。`,"botverification.explainOfficial":`官方认证标记是另一套机制,由平台在“官方认证”板块授予。两者分别存储、分别显示、分别撤销,任何一方都不代表另一方。`,"botverification.openOfficial":`官方认证`,"botverification.manageMissing":`本会话可以查看本板块并裁决申请,但不能修改认证方或图标目录——那需要 botverification.manage 权限。`,"botverification.tabRequests":`申请`,"botverification.tabVerifiers":`认证方`,"botverification.tabIcons":`图标目录`,"botverification.tabMarks":`已授予的标记`,"botverification.queueTitle":`申请队列`,"botverification.queueHint":`对象持有者向认证机器人提交的申请。计数覆盖整个队列,而不是下面这一页。`,"botverification.searchPlaceholder":`申请 ID、对象 ID、用户名或标题`,"botverification.statusAll":`全部状态`,"botverification.status.pending":`待处理`,"botverification.status.approved":`已通过`,"botverification.status.rejected":`已拒绝`,"botverification.status.revoked":`标记已撤销`,"botverification.peer.user":`账号`,"botverification.peer.channel":`频道`,"botverification.peerType":`对象类型`,"botverification.peerTypeAll":`全部类型`,"botverification.verifier":`认证方`,"botverification.verifierAll":`全部认证方`,"botverification.verifierID":`认证机器人 ID`,"botverification.applicant":`申请人`,"botverification.applicantID":`用户 ID`,"botverification.target":`对象`,"botverification.targetTitle":`标题`,"botverification.targetID":`对象 ID`,"botverification.reason":`申请理由`,"botverification.requestedDescription":`申请的说明文字`,"botverification.description":`说明`,"botverification.createdAt":`提交时间`,"botverification.company":`公司`,"botverification.companyPlaceholder":`Acme Verification Ltd`,"botverification.bot":`机器人`,"botverification.icon":`图标`,"botverification.iconDocument":`文档 ID`,"botverification.iconName":`名称`,"botverification.markCount":`标记数`,"botverification.grantedBy":`授予人`,"botverification.notProvided":`未设置`,"botverification.verifiersTitle":`认证机器人`,"botverification.verifiersHint":`获准发放自有标记的机器人。认证方身份按部署授予,所以这里的每一行都是运营人员手动打开的“标记发放机”。`,"botverification.grantTitle":`授予认证方身份`,"botverification.updateTitle":`更新认证方`,"botverification.grantHint":`机器人会拿到目录中的一个图标和一个用于背书的公司名。同一个接口也用于更新已有认证方,因此需要带上版本号。`,"botverification.grantBot":`机器人`,"botverification.grantIcon":`目录中的图标`,"botverification.grantIconPick":`选择图标`,"botverification.defaultDescription":`默认说明`,"botverification.defaultDescriptionPlaceholder":`由 Acme 认证`,"botverification.canModify":`允许认证方为每个对象单独改写说明`,"botverification.canModifyShort":`自定义说明`,"botverification.canModifyHint":`对应 botVerifierSettings.can_modify_custom_description:关闭时,该认证方发放的每个标记都使用上面的默认说明,无论申请人写了什么。`,"botverification.noActiveIcons":`目录中没有启用的图标,无法授予。请先在图标目录中添加。`,"botverification.grantNote":`记录存在且处于启用状态后,该机器人即可开始标记对象。`,"botverification.grant":`授予认证方身份`,"botverification.update":`更新认证方`,"botverification.editing":`正在更新 {bot} —— 版本 {version} 作为乐观锁一起提交,若该行已被他人改动则请求被拒绝,而不是被覆盖。`,"botverification.cancelEdit":`取消更新`,"botverification.edit":`编辑`,"botverification.enable":`启用`,"botverification.disable":`停用`,"botverification.enabled":`已启用`,"botverification.disabled":`已停用`,"botverification.disableHint":`停用是针对单个认证方的紧急开关:已发放的标记继续显示,但该机器人不能再标记新对象,其设置也不再投射到 botInfo。`,"botverification.revokeVerifier":`撤销身份`,"botverification.revokeVerifierHint":`撤销认证方身份会删除该行以及它发放过的所有标记——图标会同时从它的全部对象上消失。`,"botverification.iconsTitle":`图标目录`,"botverification.iconsHint":`认证方可用于标记的自定义表情文档。除此之外的任何东西都不能作为图标,所以错误的标记在这里被拦下,而不是事后修补。`,"botverification.addIconTitle":`添加或重命名图标`,"botverification.addIconHint":`文档 ID 必须对应本部署上真实存在的自定义表情文档;“表情”板块会列出它们及其 ID。添加一个已存在的 ID 会重命名它,而不是新建一条。`,"botverification.iconNamePlaceholder":`Acme 蓝标`,"botverification.iconOwner":`归属`,"botverification.iconOwnerShared":`共享`,"botverification.iconOwnerHint":`共享图标可以授予任何认证方;指定归属后则只保留给那一个机器人。`,"botverification.iconDocumentHint":`指向不存在文档的 ID 会产生“隐形标记”:数据库里对象已被标记,而客户端什么也画不出来。`,"botverification.addIconNote":`添加图标本身不授予任何东西,只是让该文档可被授予。`,"botverification.addIcon":`保存图标`,"botverification.iconActive":`启用`,"botverification.iconInactive":`已下架`,"botverification.usedBy":`使用中的认证方`,"botverification.activateIcon":`启用`,"botverification.deactivateIcon":`下架`,"botverification.deactivateIconHint":`下架后该图标不能再授予给新的认证方。已经带着它的标记不受影响:图标在授予时就复制到了标记上。`,"botverification.marksTitle":`已授予的标记`,"botverification.marksHint":`当前带有第三方标记的所有对象,无论由谁授予——运营裁决、认证机器人自己,或对象持有者通过 bots.setCustomVerification。`,"botverification.markSearchPlaceholder":`对象 ID、用户名或标题`,"botverification.revokeMark":`移除标记`,"botverification.revokeMarkHint":`移除标记会清除对象上的图标和说明。对应的申请仍保留历史记录。`,"botverification.loadingDetail":`正在加载申请…`,"botverification.detailTitle":`申请 #{id}`,"botverification.detailEyebrow":`第三方认证 / 审核`,"botverification.conflict":`另一位管理员已经改动过这份申请。数据已重新加载——请先确认状态再做决定。`,"botverification.markActive":`标记生效中`,"botverification.markInactive":`对象上没有标记`,"botverification.markActiveHint":`该对象已带有此认证方的标记;通过申请只会刷新说明并记录这次决定。`,"botverification.verifierSection":`认证方`,"botverification.verifierHint":`对象将要佩戴其图标的公司,按其当前记录显示。`,"botverification.openVerifier":`打开认证机器人`,"botverification.verifierMissing":`认证方记录已不存在:这份申请提交后其身份被撤销了。没有可授予的图标,因此这份申请只能被拒绝。`,"botverification.verifierDisabledHint":`该认证方已被停用,在运营人员重新启用之前不能标记任何新对象。`,"botverification.targetSection":`对象`,"botverification.targetHint":`图标将要附加到的账号、机器人或频道。`,"botverification.openTarget":`打开对象`,"botverification.applicantSection":`申请人`,"botverification.applicantHint":`向认证机器人提交这份申请的人。`,"botverification.openApplicant":`打开账号`,"botverification.requestSection":`申请内容`,"botverification.requestHint":`申请人填写的内容,按纯文本呈现。`,"botverification.correlationID":`关联 ID`,"botverification.markPreview":`标记将显示的说明`,"botverification.markPreviewHint":`与后端的解析规则一致:只有当该认证方被允许自定义说明时才使用申请人的文字,否则使用认证方的默认说明。`,"botverification.descriptionIgnoredHint":`该认证方不能为单个对象设置说明,因此申请的文字被忽略,改用默认说明。`,"botverification.decisionSection":`决定`,"botverification.decisionHint":`谁做了什么决定,以及用了什么措辞。`,"botverification.decidedBy":`裁决人`,"botverification.approvedAt":`通过时间`,"botverification.rejectedAt":`拒绝时间`,"botverification.version":`版本(乐观锁)`,"botverification.decisionReason":`决定理由`,"botverification.noDecision":`尚未裁决`,"botverification.internalNote":`内部备注`,"botverification.adminOnly":`仅管理员可见`,"botverification.internalNotePlaceholder":`留给其他管理员的交接说明`,"botverification.internalNoteHint":`可选。与决定一起保存,仅管理员可见——绝不会发给申请人。`,"botverification.actionDock":`裁决`,"botverification.noActions":`当前状态没有可执行的操作。`,"botverification.approve":`通过`,"botverification.approveHint":`把认证方的图标放到对象名称前面,把说明放进资料页,并通知申请人。`,"botverification.reject":`拒绝`,"botverification.rejectHint":`理由必填:申请人看到的就是这段文字,所以请写清到底缺了什么。`,"botverification.dangerZone":`危险操作`,"botverification.revokeRequest":`撤销标记`,"botverification.revokeRequestHint":`从对象上移除图标和说明,并把申请关闭为“已撤销”。对象若持有官方认证标记,不受影响。`,"botverification.revokeNoMark":`该对象当前没有标记——撤销只会关闭这份申请。`,"botverification.rosterDenied":`服务器拒绝了本会话读取认证方名单与图标目录(403),因此这两个列表为空——申请仍然可以审核。`},ru:{"app.adminConsole":`Панель администратора`,"app.localAccess":`Локальный доступ`,"app.title":`telesrv admin`,"common.actions":`Действия`,"common.admins":`Администраторы`,"common.backToList":`Назад к списку`,"common.channel":`Канал`,"common.channelOrGroup":`Канал / Группа`,"common.clear":`Очистить`,"common.close":`Закрыть`,"common.count":`Количество`,"common.deleted":`Удалено`,"common.detail":`Детали`,"common.device":`Устройство`,"common.disabled":`Отключено`,"common.enabled":`Включено`,"common.fromPeer":`От пира`,"common.group":`Группа`,"common.id":`ID`,"common.limit":`Лимит`,"common.loading":`Загрузка…`,"common.member":`Участник`,"common.members":`Участники`,"common.messageId":`ID сообщения`,"common.name":`Имя`,"common.no":`Нет`,"common.noResults":`Нет результатов`,"common.none":`Нет`,"common.normal":`Обычный`,"common.operations":`Операции`,"common.owner":`Владелец`,"common.platform":`Платформа`,"common.refresh":`Обновить`,"common.search":`Поиск`,"common.sender":`Отправитель`,"common.status":`Статус`,"common.survived":`Уцелело`,"common.time":`Время`,"common.type":`Тип`,"common.updatedAt":`Обновлено`,"common.username":`Имя пользователя`,"common.valid":`Действителен`,"common.verified":`Подтверждён`,"common.views":`Просмотры`,"common.yes":`Да`,"route.accounts":`Аккаунты`,"route.accountsSubtitle":`Консоль / Аккаунты`,"route.channels":`Супергруппы и каналы`,"route.channelsSubtitle":`Консоль / Каналы`,"route.moderation":`Жалобы и модерация`,"route.moderationSubtitle":`Консоль / Модерация`,"route.dashboard":`Панель управления`,"route.dashboardSubtitle":`Консоль / Обзор`,"route.messages":`Аудит сообщений`,"route.messagesSubtitle":`Консоль / Сообщения`,"route.gifts":`Звёздные подарки`,"route.giftsSubtitle":`Консоль / Звёздные подарки`,"route.giveGifts":`Выдача подарков`,"route.giveGiftsSubtitle":`Консоль / Выдача подарков`,"layout.navigation":`Навигация`,"layout.primaryNav":`Основное меню`,"layout.dashboard":`Обзор`,"layout.accounts":`Аккаунты`,"layout.channels":`Супергруппы / Каналы`,"layout.moderation":`Жалобы / Модерация`,"layout.messages":`Сообщения`,"layout.gifts":`Звёздные подарки`,"layout.giveGifts":`Выдача подарков`,"layout.privateMessages":`Личные`,"layout.groupMessages":`Группы`,"layout.runtime":`Среда выполнения`,"layout.adminBackend":`Админ-бэкенд`,"layout.ready":`Готов`,"layout.pgRead":`Чтение из PG`,"layout.readOnly":`Только чтение`,"layout.writeOps":`Операции записи`,"layout.dryRun":`Тестовый запуск`,"layout.actor":`Вы вошли как: {actor}`,"layout.logout":`Выйти`,"language.en":`EN`,"language.zh":`中文`,"language.ru":`RU`,"theme.switchToDark":`Тёмная тема`,"theme.switchToLight":`Светлая тема`,"login.heading":`Панель администратора`,"login.body":`Введите учётные данные для входа в консоль.`,"login.secret":`Пароль или токен администратора`,"login.submit":`Войти`,"login.submitting":`Вход…`,"dashboard.eyebrow":`Состояние системы`,"dashboard.title":`Обзор консоли`,"dashboard.readPath":`Путь чтения`,"dashboard.readPathValue":`PG только для чтения`,"dashboard.writePath":`Путь записи`,"dashboard.executionPolicy":`Политика выполнения`,"dashboard.dryRunFirst":`Сначала тестовый запуск`,"dashboard.accountsText":`Статус аккаунтов, Premium, подтверждение, сессии.`,"dashboard.channelsText":`Публичные каналы и группы, число участников, статус подтверждения.`,"dashboard.messagesText":`Ящики сообщений, обновления, состояние исходящих.`,"dashboard.strip.dryRun":`Все опасные действия начинаются с тестового запуска`,"dashboard.strip.token":`Браузер никогда не сохраняет внутренние токены`,"dashboard.strip.pagination":`Списки используют курсорную пагинацию`,"dashboard.strip.snapshot":`Детальные страницы сохраняют моментальные снимки исходного состояния`,"account.pageTitle":`Аккаунты`,"account.queryResults":`Результаты поиска`,"account.recentActive":`Недавно активные аккаунты`,"account.currentPage":`Аккаунты на странице`,"account.onlineDevices":`Активные сессии устройств`,"account.premium":`Premium`,"account.frozen":`Заморожен`,"account.searchPlaceholder":`ID пользователя / телефон / имя пользователя`,"account.userID":`ID пользователя`,"account.phone":`Телефон`,"account.lastActive":`Последняя активность`,"account.notVerified":`Не подтверждён`,"account.notPremium":`Без Premium`,"account.premiumUntil":`Premium истекает`,"account.starsBalance":`Баланс Звёзд`,"account.startingGrantApplied":`стартовый бонус начислен`,"account.startingGrantPending":`ожидает стартового бонуса`,"account.activeSessions":`Авторизованные устройства`,"account.accountFlags":`Флаги аккаунта`,"account.restriction":`Ограничение`,"account.restricted":`Ограничен`,"account.createdAt":`Создан`,"account.detailTitle":`Аккаунт #{id}`,"account.profile":`Профиль аккаунта`,"account.loadingDetail":`Загрузка данных аккаунта`,"account.waitingData":`Ожидание данных`,"account.noUsername":`Нет имени пользователя`,"account.noPhone":`Нет телефона`,"account.accountFrozen":`Аккаунт заморожен`,"account.accountActive":`Аккаунт активен`,"account.authorizationsTitle":`Авторизованные устройства`,"account.authorizationsCount":`Авторизаций: {count}`,"account.recentAdminOps":`Последние действия администратора`,"account.recent30Audit":`Последние 30 записей аудита`,"account.actionDock":`Действия с аккаунтом`,"account.freezeAccount":`Заморозить аккаунт`,"account.updateFreeze":`Обновить параметры заморозки`,"account.unfreezeAccount":`Разморозить аккаунт`,"account.freezeSince":`Заморожен с`,"account.freezeUntil":`Срок подачи апелляции`,"account.freezeUntilAria":`Срок подачи апелляции на заморозку`,"account.freezeAppealURL":`URL для апелляции`,"account.freezeAppealURLAria":`URL для апелляции на заморозку`,"account.premiumMonths":`Срок действия Premium (в месяцах)`,"account.premiumMonthsAria":`Указать срок действия Premium в месяцах`,"account.setPremium":`Выдать Premium`,"account.clearPremium":`Снять Premium`,"account.starsAmount":`Количество Звёзд`,"account.starsAmountAria":`Указать количество начисляемых Звёзд`,"account.grantStars":`Начислить Звёзды`,"account.setVerified":`Подтвердить аккаунт`,"account.clearVerified":`Снять подтверждение`,"channel.pageTitle":`Супергруппы и каналы`,"channel.recentUpdated":`Недавно обновлённые`,"channel.currentPage":`Объекты на странице`,"channel.megagroups":`Супергруппы`,"channel.broadcasts":`Каналы`,"channel.verifiedCount":`Подтверждённые`,"channel.searchPlaceholder":`ID канала / имя пользователя / название`,"channel.channelID":`ID канала`,"channel.kind":`Тип`,"channel.title":`Название`,"channel.pts":`PTS`,"channel.detailProfile":`Профиль канала`,"channel.loadingDetail":`Загрузка данных канала`,"channel.creator":`Создатель: {id}`,"channel.governance":`Модерация`,"channel.governanceValue":`Заблокировано {banned} / Исключено {kicked}`,"channel.flags":`Флаги канала`,"channel.rawRow":`Исходная строка БД`,"channel.rawRowText":`Снимок базы данных только для чтения`,"channel.actionDock":`Действия с каналом`,"channel.setVerified":`Подтвердить канал`,"channel.clearVerified":`Снять подтверждение`,"channel.kind.broadcast":`Канал`,"channel.kind.forum":`Супергруппа / Форум`,"channel.kind.megagroup":`Супергруппа`,"channel.kind.generic":`Канал / Группа`,"route.bots":`Боты`,"route.botsSubtitle":`Консоль / Боты`,"layout.bots":`Боты`,"bots.pageTitle":`Боты`,"bots.queryResults":`Результаты поиска`,"bots.recent":`Недавно созданные боты`,"bots.currentPage":`Боты на странице`,"bots.banned":`Забанен`,"bots.active":`Активен`,"bots.createTitle":`Создать системного бота`,"bots.createHint":`Создаёт бота, принадлежащего указанному пользователю. Токен показывается один раз после подтверждения.`,"bots.ownerUserID":`ID владельца`,"bots.name":`Отображаемое имя`,"bots.namePlaceholder":`например, Service Bot`,"bots.username":`Имя пользователя`,"bots.usernameHint":`Имя пользователя: 5–32 символа, обязательно оканчивается на «bot».`,"bots.create":`Создать бота`,"bots.searchPlaceholder":`ID бота / имя пользователя`,"bots.botID":`ID бота`,"bots.owner":`Владелец`,"bots.status":`Статус`,"bots.detailTitle":`Бот #{id}`,"bots.profile":`Профиль бота`,"bots.loadingDetail":`Загрузка данных бота`,"bots.unnamed":`Без имени`,"bots.restriction":`Ограничение`,"bots.actionDock":`Действия с ботом`,"bots.banUntil":`Забанить до`,"bots.ban":`Забанить бота`,"bots.updateBan":`Обновить бан`,"bots.unban":`Разбанить бота`,"bots.type":`Тип`,"bots.system":`Системный`,"bots.user":`Пользовательский`,"bots.delete":`Удалить бота`,"bots.deleteHint":`Безвозвратно удаляет созданного пользователем бота и аннулирует его токен. Действие необратимо.`,"bots.systemHint":`Системные боты встроены и не могут быть удалены.`,"flags.scam":`SCAM`,"flags.fake":`FAKE`,"flags.setScam":`Пометить как SCAM`,"flags.clearScam":`Снять метку SCAM`,"flags.setFake":`Пометить как FAKE`,"flags.clearFake":`Снять метку FAKE`,"attr.attributes":`Атрибуты`,"attr.settings":`Настройки`,"attr.username":`Имя пользователя`,"attr.setUsername":`Задать имя пользователя`,"attr.setSupport":`Пометить как support`,"attr.clearSupport":`Снять support`,"attr.forProfile":`Цвет профиля`,"attr.hasColor":`Включить цвет`,"attr.colorIndex":`Индекс цвета`,"attr.bgEmojiID":`ID фонового эмодзи`,"attr.setColor":`Задать цвет`,"attr.emojiDocID":`ID документа эмодзи`,"attr.emojiUntil":`До (unix, 0 = бессрочно)`,"attr.setEmojiStatus":`Задать emoji-статус`,"attr.gigagroup":`Гигагруппа`,"attr.antispam":`Агрессивный антиспам`,"attr.participantsHidden":`Скрыть участников`,"attr.noforwards":`Запретить пересылку`,"attr.joinToSend":`Вступление для отправки`,"attr.joinRequest":`Вступление по заявке`,"attr.slowmode":`Медленный режим (сек)`,"attr.applySettings":`Применить настройки`,"route.emoji":`Emoji`,"route.emojiSubtitle":`Консоль / Emoji`,"layout.emoji":`Emoji`,"emoji.pageTitle":`Кастом-эмодзи`,"emoji.queryResults":`Результаты поиска`,"emoji.recent":`Каталог кастом-эмодзи`,"emoji.currentPage":`Эмодзи на странице`,"emoji.searchPlaceholder":`ID документа или эмодзи`,"emoji.copyID":`Скопировать ID документа`,"emoji.noSet":`Без набора`,"emoji.hint":`ID документов отсюда можно вставлять в поле Emoji-статуса в профилях аккаунтов, ботов и каналов.`,"messages.privateTitle":`Личные сообщения`,"messages.privateEyebrow":`Личные ящики сообщений`,"messages.groupTitle":`Групповые сообщения`,"messages.groupEyebrow":`Сообщения супергрупп и каналов`,"messages.selectPrivatePeers":`Сначала найдите и выберите владельца и собеседника`,"messages.selectChannel":`Сначала найдите и выберите супергруппу или канал`,"messages.ownerUser":`Пользователь-владелец`,"messages.peerUser":`Собеседник`,"messages.beforeDatePlaceholder":`курсор before_date`,"messages.beforeIDPlaceholder":`курсор before_msg_id`,"messages.limitPlaceholder":`лимит <= 100`,"messages.searchMessages":`Поиск сообщений`,"messages.nextPage":`Следующая страница`,"messages.currentPage":`Сообщения на странице`,"messages.deleted":`Удалено`,"messages.outgoing":`Исходящее`,"messages.incoming":`Входящее`,"messages.ownerPeer":`Владелец / Собеседник`,"messages.deleteSelected":`Удалить выбранные сообщения`,"messages.idsPlaceholder":`ID сообщений через запятую`,"messages.revoke":`Удалить для обеих сторон`,"messages.previewDelete":`Тестовое удаление`,"messages.clearHistory":`Очистить историю личной переписки`,"messages.maxIDPlaceholder":`граница max_id`,"messages.maxBatchesPlaceholder":`max_batches`,"messages.justClear":`Очистить только у себя`,"messages.previewClearHistory":`Тестовая очистка истории`,"messages.direction":`Направление`,"messages.body":`Текст сообщения`,"messages.privateDetailTitle":`Сообщение #{id}`,"messages.detailEyebrow":`Детали сообщения`,"messages.backPrivate":`Назад к личным сообщениям`,"messages.backGroup":`Назад к групповым сообщениям`,"messages.ownerPeerTitle":`Владелец {owner} · Собеседник {peer}`,"messages.senderSubtitle":`Отправитель {sender} · {date}`,"messages.boxID":`ID ящика сообщений`,"messages.privateMessageID":`ID личного сообщения`,"messages.messageSender":`Отправитель сообщения`,"messages.messageBox":`Ящик сообщений`,"messages.dialogRow":`Строка диалога`,"messages.privateRow":`Строка личного сообщения`,"messages.channelMessageRow":`Строка сообщения канала`,"messages.channelRow":`Строка канала`,"messages.userUpdateEvents":`События обновления пользователей`,"messages.channelUpdateEvents":`События обновления каналов`,"messages.eventJson":`JSON события`,"messages.dispatchOutbox":`Очередь отправки (Outbox)`,"messages.messageBoxesSnapshot":`Снимок message_boxes только для чтения`,"messages.dialogSnapshot":`Снимок dialogs только для чтения`,"messages.privateSnapshot":`Снимок private_messages только для чтения`,"messages.channelMessagesSnapshot":`Снимок channel_messages только для чтения`,"messages.channelSnapshot":`Снимок channels только для чтения`,"messages.userEventsSource":`постоянные user_update_events`,"messages.channelEventsSource":`постоянные channel_update_events`,"messages.outboxSource":`онлайн/офлайн dispatch_outbox`,"messages.attempts":`Попытки`,"messages.deleteThis":`Удалить это сообщение`,"messages.groupDetailTitle":`Групповое сообщение #{id}`,"messages.channelGroupTitle":`Канал / Группа {id}`,"messages.mediaCount":`С медиафайлами`,"messages.channelPosts":`Посты канала`,"messages.channelGroup":`Канал / Группа`,"messages.pinned":`Закреплено`,"messages.channelPost":`Пост в канале`,"gifts.pageTitle":`Каталог звёздных подарков`,"giveGift.action":`Выдать`,"giveGift.eyebrow":`Выдача подарка · без списания`,"giveGift.title":`Выдать подарок`,"giveGift.recipientKind":`Тип получателя`,"giveGift.recipientUser":`Пользователь`,"giveGift.recipientChannel":`Канал`,"giveGift.pickUser":`Получатель (пользователь)`,"giveGift.pickChannel":`Получатель (канал)`,"giveGift.recipientRequired":`Сначала выберите получателя`,"giveGift.sender":`ID аккаунта-отправителя`,"giveGift.senderHint":`Подарки всегда отправляются от системного аккаунта 777000 (Telesrv).`,"giveGift.message":`Сообщение к подарку (необязательно)`,"giveGift.messagePlaceholder":`Показывается вместе с подарком`,"giveGift.hideName":`Скрыть имя отправителя от получателя`,"giveGift.upgrade":`Выдать как улучшенный коллекционный`,"giveGift.upgradeNote":`Подарок будет отчеканен как уникальный коллекционный. Ниже можно выбрать конкретные атрибуты или оставить «Случайно» для выбора из опубликованного пула. Номер присваивается автоматически. Требуется опубликованное коллекционное улучшение с остатком тиража.`,"giveGift.model":`Модель`,"giveGift.pattern":`Узор`,"giveGift.backdrop":`Фон`,"giveGift.random":`Случайно`,"giveGift.confirm":`Выдать подарок`,"giveGifts.pageTitle":`Выдача подарков`,"giveGifts.eyebrow":`Выдача каталожных подарков любому пользователю или каналу`,"giveGifts.available":`Доступно подарков`,"giveGifts.sender":`Отправитель по умолчанию`,"giveGifts.searchPlaceholder":`Поиск по названию или ID подарка`,"giveGifts.hint":`Выберите подарок для выдачи. Выдача бесплатна и по умолчанию отправляется от системного аккаунта 777000 (Telesrv).`,"giveGifts.pickGift":`Выберите подарок`,"giveGifts.selectPrompt":`Выберите подарок из списка, чтобы начать.`,"gifts.eyebrow":`Каталог, неизменяемые версии и файлы анимаций`,"gifts.total":`Подарков в каталоге`,"gifts.enabled":`Включено`,"gifts.received":`Полученные подарки`,"gifts.formats":`Поддерживаемые форматы`,"gifts.add":`Добавить подарок`,"gifts.searchPlaceholder":`Поиск по ID подарка, названию или формату`,"gifts.listSummary":`Показано {shown} из {total}`,"gifts.idRevision":`ID / Версия`,"gifts.price":`Цена / Конвертация`,"gifts.importTitle":`Импорт звёздного подарка`,"gifts.importEyebrow":`Управление каталогом подарков`,"gifts.newRevision":`Создать версию для подарка #{id}`,"gifts.importHint":`Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.`,"gifts.officialSource":`Официальный снимок`,"gifts.fileSource":`Загрузить файл`,"gifts.officialHint":`Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.`,"gifts.officialSearch":`Поиск по ID или названию официального подарка`,"gifts.officialSelect":`Выберите официальный подарок`,"gifts.officialRequired":`Сначала выберите официальный подарок`,"gifts.officialResults":`Показано {shown} из {total}`,"gifts.officialCategoryLabel":`Категория возможностей официального подарка`,"gifts.officialCategory.all":`Все`,"gifts.officialCategory.upgrade":`Можно улучшить`,"gifts.officialCategory.craft":`Можно создать`,"gifts.officialCategory.basic":`Нельзя улучшить`,"gifts.officialUnnamed":`Официальный подарок без названия #{id}`,"gifts.officialAttributes":`Атрибутов: {count}`,"gifts.canUpgrade":`Можно улучшить`,"gifts.cannotUpgrade":`Нельзя улучшить`,"gifts.canCraft":`Можно создать`,"gifts.cannotCraft":`Нельзя создать`,"gifts.officialEmpty":`Нет подарков, соответствующих категории и поиску.`,"gifts.includeCollectible":`Импортировать полный пул коллекционных предметов, включая созданные модели`,"gifts.animation":`Файл анимации`,"gifts.filePrompt":`Перетащите или выберите файл TGS / Lottie`,"gifts.fileHint":`TGS, JSON или Lottie · файл проверяется перед импортом`,"gifts.chooseFile":`Выбрать файл`,"gifts.changeFile":`Изменить файл`,"gifts.title":`Отображаемое название`,"gifts.titlePlaceholder":`например, Праздничная звезда`,"gifts.stars":`Цена в Звёздах`,"gifts.convertStars":`Звёзд при конвертации`,"gifts.sortOrder":`Порядок сортировки`,"gifts.reason":`Причина для аудита`,"gifts.reasonPlaceholder":`Кратко опишите причину импорта этого подарка`,"gifts.enableAfterImport":`Включить после импорта`,"gifts.validate":`Тестовая проверка`,"gifts.confirmImport":`Подтвердить импорт`,"gifts.stepDetails":`Файл и описание`,"gifts.stepValidate":`Тестовая проверка`,"gifts.stepImport":`Подтверждение импорта`,"gifts.fileRequired":`Сначала выберите файл TGS или Lottie`,"gifts.source":`Источник`,"gifts.replace":`Новая версия`,"gifts.disable":`Отключить`,"gifts.enable":`Включить`,"gifts.empty":`Звёздные подарки ещё не импортированы.`,"gifts.emptyHint":`Импортируйте первую анимацию, чтобы начать наполнение каталога.`,"gifts.validationReady":`Проверка пройдена`,"gifts.validationHint":`Проверьте нормализованные метаданные и подтвердите импорт.`,"gifts.confirmState":`Применить проверенные изменения состояния к подарку #{id}?`,"collectibles.manage":`Пул атрибутов`,"collectibles.title":`Пул коллекционных предметов · Подарок #{id}`,"collectibles.eyebrow":`Уникальные атрибуты подарка`,"collectibles.activeRevision":`Опубликованная версия {revision}`,"collectibles.published":`Опубликовано`,"collectibles.noPool":`Нет опубликованного пула коллекционных предметов`,"collectibles.noPoolHint":`Опубликуйте модели, узоры и фоны для активации улучшений.`,"collectibles.publishNew":`Опубликовать новую неизменяемую версию`,"collectibles.immutableHint":`Тестовый запуск проверяет каждый файл и структуру набора атрибутов перед активацией версии.`,"collectibles.upgradeStars":`Цена улучшения в Звёздах`,"collectibles.supply":`Уникальный тираж`,"collectibles.slug":`Публичный префикс ссылки (slug)`,"collectibles.models":`Модели`,"collectibles.patterns":`Узоры`,"collectibles.backdrops":`Фоны`,"collectibles.model":`Модель`,"collectibles.pattern":`Узор`,"collectibles.backdrop":`Фон`,"collectibles.rarity":`Редкость ‰`,"collectibles.rarityHint":`В каждой категории должно быть не менее двух атрибутов. Значения в промилле задают относительные веса обычного улучшения; при добавлении или удалении они перераспределяются до суммы 1000.`,"collectibles.minimumAttributes":`Модели, узоры и фоны должны содержать не менее двух атрибутов в каждой категории.`,"collectibles.duplicateBackdropID":`ID фонов в одном наборе должны быть уникальными.`,"collectibles.colorHint":`Цвета сохраняются как 24-битные RGB-значения.`,"collectibles.addAttribute":`Добавить`,"collectibles.remove":`Удалить атрибут`,"collectibles.fileRequired":`Для каждой модели и узора требуется файл TGS или Lottie.`,"collectibles.backdropID":`ID фона`,"collectibles.color.center":`Центр`,"collectibles.color.edge":`Край`,"collectibles.color.pattern":`Узор`,"collectibles.color.text":`Текст`,"collectibles.validationReady":`Пул атрибутов корректен`,"collectibles.validationHint":`Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.`,"collectibles.publish":`Опубликовать версию`,"moderation.casesEyebrow":`Модерация / Обращения`,"moderation.currentQueue":`Текущая очередь`,"moderation.criticalCases":`Критические обращения`,"moderation.pendingOrFailed":`Ожидающие / неудачные действия`,"moderation.statusFilter":`Фильтр по статусу обращения`,"moderation.statusFilter.active":`Активная очередь`,"moderation.statusFilter.all":`Все статусы`,"moderation.assignee":`Модератор`,"moderation.allAssignees":`Оставьте пустым для всех`,"moderation.case":`Обращение`,"moderation.target":`Объект`,"moderation.severity":`Важность`,"moderation.reportsAndReporters":`Жалобы / Авторы`,"moderation.latestReport":`Последняя жалоба`,"moderation.review":`Проверить`,"moderation.status.open":`Открыто`,"moderation.status.in_review":`На рассмотрении`,"moderation.status.action_pending":`Ожидает действия`,"moderation.status.action_failed":`Ошибка действия`,"moderation.status.resolved":`Решено`,"moderation.status.dismissed":`Отклонено`,"moderation.status.appeal_review":`Рассмотрение апелляции`,"moderation.severity.low":`Низкая`,"moderation.severity.medium":`Средняя`,"moderation.severity.high":`Высокая`,"moderation.severity.critical":`Критическая`,"moderation.targetType.user":`Аккаунт`,"moderation.targetType.chat":`Группа`,"moderation.targetType.channel":`Канал`,"moderation.caseDetailTitle":`Проверка обращения #{id}`,"moderation.caseDetailEyebrow":`Модерация / Детали обращения`,"moderation.backToQueue":`Назад к очереди`,"moderation.loadingCase":`Загрузка обращения…`,"moderation.versionAndUpdated":`Версия {version} · Обновлено {time}`,"moderation.reportCount":`Жалобы`,"moderation.reportCountValue":`Жалоб: {reports}; авторов: {reporters}`,"moderation.firstAndLatestReport":`Первая / последняя жалоба`,"moderation.evidence":`Материалы жалобы`,"moderation.evidenceHint":`Показываются последние 100 жалоб; снимки фиксируются при приёме жалобы.`,"moderation.sourceAndReason":`Источник / Причина`,"moderation.reporter":`Автор жалобы`,"moderation.option":`Вариант`,"moderation.decisionAudit":`Аудит решений и действий`,"moderation.decisionAuditHint":`Действия выполняются идемпотентно арендующим worker-процессом; при сбое сохраняются ошибка и число попыток.`,"moderation.appeals":`Апелляции`,"moderation.caseActions":`Действия с обращением`,"moderation.renewClaim":`Продлить назначение`,"moderation.claimCase":`Взять на проверку`,"moderation.reviewReason":`Причина решения`,"moderation.decisionPreset":`Шаблон решения`,"moderation.preset.noViolation":`Нет нарушения (отклонить жалобу)`,"moderation.preset.scam":`Пометить как SCAM`,"moderation.preset.fake":`Пометить как FAKE`,"moderation.preset.freeze":`Заморозить аккаунт`,"moderation.preset.scamFreeze":`SCAM + заморозка`,"moderation.preset.fakeFreeze":`FAKE + заморозка`,"moderation.preset.deleteMessages":`Удалить сообщения из материалов`,"moderation.preset.deleteAccount":`Удалить аккаунт`,"moderation.evidenceMessageIDs":`ID сообщений из материалов (через запятую)`,"moderation.privateOwnerUserID":`owner_user_id личного чата`,"moderation.revokeForBoth":`Удалить у обеих сторон`,"moderation.evidenceValidationHint":`Сервер повторно проверит, что каждый ID сообщения присутствует в неизменяемых материалах этого обращения.`,"moderation.failedActionHint":`Действие выполнено частично, поэтому решение нельзя сразу сменить на отсутствие нарушения. Выберите новое действие для повтора; аудит предыдущего сбоя сохранится.`,"moderation.retryAction":`Повторить действие`,"moderation.submitDecision":`Отправить решение`,"moderation.appealReviewTitle":`Рассмотрение апелляции #{id}`,"moderation.automaticRemedy":`Автовосстановление после одобрения`,"moderation.irreversibleAppealHint":`Обращение содержит завершённое необратимое удаление. Его нельзя отметить как одобренное и восстановленное; отклоните или передайте на ручную обработку.`,"moderation.denyAppeal":`Отклонить апелляцию`,"moderation.grantAppeal":`Одобрить апелляцию`,"moderation.reasonRequired":`Укажите причину решения.`,"moderation.appealReasonRequired":`Укажите причину рассмотрения апелляции.`,"moderation.privateDeleteValidation":`Для удаления личных сообщений нужны корректные ID сообщений из материалов и owner_user_id автора жалобы.`,"moderation.channelDeleteValidation":`Для удаления сообщений канала нужен хотя бы один корректный ID сообщения из материалов.`,"moderation.confirmDecision":`Отправить решение «{decision}»? Действие будет выполнено через устойчивую очередь.`,"moderation.confirmGrantAppeal":`Одобрить эту апелляцию?`,"moderation.confirmDenyAppeal":`Отклонить эту апелляцию?`,"moderation.remedy.clearFlags":`Снять SCAM / FAKE`,"moderation.remedy.unfreeze":`Разморозить аккаунт`,"moderation.remedy.none":`Восстановление не требуется`,"moderation.source.account_peer":`Аккаунт / пир`,"moderation.source.profile_photo":`Фото профиля`,"moderation.source.messages_spam":`Спам в сообщениях`,"moderation.source.messages":`Сообщения`,"moderation.source.encrypted_spam":`Спам в секретном чате`,"moderation.source.reaction":`Реакция`,"moderation.source.channel_spam":`Спам в канале`,"moderation.source.story":`История`,"moderation.source.ephemeral":`Исчезающее медиа`,"moderation.source.sponsored":`Рекламное сообщение`,"moderation.source.antispam_false_positive":`Ложное срабатывание антиспама`,"moderation.reason.spam":`Спам`,"moderation.reason.violence":`Насилие`,"moderation.reason.pornography":`Порнография`,"moderation.reason.child_abuse":`Жестокое обращение с детьми`,"moderation.reason.other":`Другое`,"moderation.reason.copyright":`Авторские права`,"moderation.reason.geo_irrelevant":`Не относится к региону`,"moderation.reason.fake":`Подделка`,"moderation.reason.illegal_drugs":`Незаконные наркотики`,"moderation.reason.personal_details":`Персональные данные`,"messages.msgIDsInvalid":`Некорректные ID сообщений`,"auth.device":`Устройство`,"auth.platform":`Платформа`,"auth.ip":`IP-адрес`,"auth.lastActive":`Последняя активность`,"auth.revokeCurrent":`Отозвать текущую`,"auth.keepCurrent":`Оставить текущую`,"auth.revokeAll":`Отозвать все устройства`,"picker.userPlaceholder":`Поиск по user_id / телефону / имени пользователя`,"picker.channelPlaceholder":`Поиск по channel_id / имени пользователя / названию`,"picker.verified":`Подтверждённые`,"picker.regular":`Обычные`,"action.reasonRequired":`Пожалуйста, укажите причину операции`,"action.flow":`Процесс выполнения`,"action.close":`Закрыть`,"action.stepReason":`Укажите причину`,"action.stepDryRun":`Тестовый запуск`,"action.stepConfirm":`Подтверждение выполнения`,"action.reason":`Причина операции`,"action.reasonPlaceholder":`Опишите, почему выполняется эта операция`,"action.requestPreview":`Запросить предпросмотр`,"action.result":`Результат действия`,"action.commandID":`ID команды`,"action.status":`Статус`,"action.dryRun":`Тестовый запуск`,"action.runAgain":`Повторить тестовый запуск`,"action.runDry":`Сначала выполните тестовый запуск`,"action.confirm":`Подтвердить выполнение`,"audit.id":`ID`,"audit.commandID":`ID команды`,"audit.action":`Действие`,"audit.actor":`Исполнитель`,"audit.status":`Статус`,"audit.dryRun":`Тестовый запуск`,"audit.reason":`Причина`,"audit.time":`Время`,"common.loadMore":`Показать ещё`,"route.collectibleUsernames":`Коллекционные юзернеймы`,"route.collectibleUsernamesSubtitle":`Консоль / Коллекционные юзернеймы`,"route.accountRatings":`Рейтинг аккаунтов`,"route.accountRatingsSubtitle":`Консоль / Рейтинг аккаунтов`,"layout.collectibleUsernames":`NFT-юзернеймы`,"layout.accountRatings":`Рейтинг аккаунтов`,"usernames.pageTitle":`Коллекционные юзернеймы`,"usernames.eyebrow":`NFT-юзернеймы / Реестр`,"usernames.metricLoaded":`Загружено строк`,"usernames.metricVault":`В хранилище`,"usernames.metricOwned":`У владельцев`,"usernames.metricBurned":`Сожжено`,"usernames.mintTitle":`Выпустить юзернейм`,"usernames.mintHint":`Создаёт актив вместе с записью о покупке. Оставьте «хранилище», чтобы выпустить юзернейм без владельца.`,"usernames.mint":`Выпустить юзернейм`,"usernames.mintNote":`Юзернейм, валюта и сумма обязательны — тестовый запуск сначала проверит, свободен ли юзернейм.`,"usernames.ownerKind":`Тип владельца`,"usernames.ownerVault":`Хранилище (без владельца)`,"usernames.ownerUser":`Владелец-пользователь`,"usernames.ownerChannel":`Владелец-канал`,"usernames.currency":`Валюта`,"usernames.amount":`Сумма ({currency})`,"usernames.cryptoCurrency":`Криптовалюта`,"usernames.cryptoNone":`Нет`,"usernames.cryptoAmount":`Сумма в крипте ({currency})`,"usernames.amountHint":`Сумма вводится в целых {currency}, а хранится в наименьших единицах, которые принимают API и fragment.collectibleInfo, — тогда клиент покажет именно ту цену, которую вы задали. До {decimals} знаков после запятой. Клиент покажет: {preview}.`,"usernames.amountInvalid":`Это не похоже на сумму в {currency}: только цифры и не больше {decimals} знаков после запятой.`,"usernames.inactive":`выключен`,"usernames.url":`Ссылка на маркетплейс`,"usernames.purchaseDate":`Дата покупки (UTC)`,"usernames.purchaseTime":`Время покупки (UTC)`,"usernames.searchPlaceholder":`Поиск по юзернейму`,"usernames.statusAll":`Все статусы`,"usernames.statusVault":`В хранилище`,"usernames.statusOwned":`У владельца`,"usernames.statusBurned":`Сожжён`,"usernames.price":`Цена`,"usernames.transfers":`Передачи`,"usernames.registryActive":`Активен в профиле`,"usernames.registryHidden":`Скрыт в профиле`,"usernames.loadingDetail":`Загружаем коллекционный юзернейм…`,"usernames.detailTitle":`Юзернейм {username}`,"usernames.detailEyebrow":`NFT-юзернеймы / Актив`,"usernames.assetID":`Актив №{id}`,"usernames.transferCount":`Передач: {count}`,"usernames.originalOwner":`Первый владелец`,"usernames.openOwnerAccount":`Открыть аккаунт владельца`,"usernames.openOwnerChannel":`Открыть канал владельца`,"usernames.openMarketplace":`Открыть страницу на маркетплейсе`,"usernames.transferTitle":`Передать юзернейм`,"usernames.transferHint":`Выберите получателя — передача попадёт в историю владения.`,"usernames.recipientKind":`Тип получателя`,"usernames.recipientUser":`Пользователю`,"usernames.recipientChannel":`Каналу`,"usernames.transferNote":`После подтверждения текущий владелец сразу теряет юзернейм.`,"usernames.transfer":`Передать`,"usernames.historyTitle":`История владения`,"usernames.historyHint":`Выпуск, передачи, отзывы и сжигание — в хронологическом порядке.`,"usernames.eventKind":`Событие`,"usernames.fromPeer":`От`,"usernames.toPeer":`Кому`,"usernames.actionDock":`Операции с активом`,"usernames.revoke":`Отозвать в хранилище`,"usernames.revokeHint":`Забирает юзернейм у владельца и возвращает в хранилище — позже его можно выдать снова.`,"usernames.burn":`Сжечь безвозвратно`,"usernames.burnHint":`Необратимо: юзернейм уничтожается и больше никогда не будет выдан.`,"usernames.burnedHint":`Юзернейм сожжён — операции с ним больше недоступны.`,"usernames.delete":`Удалить запись`,"usernames.deleteHint":`Стирает актив вместе с историей владения и полностью освобождает юзернейм для нового выпуска. Это для случая «выпустил не то имя»; сжигание, наоборот, историю сохраняет.`,"usernames.kind.mint":`Выпуск`,"usernames.kind.transfer":`Передача`,"usernames.kind.revoke":`Отзыв`,"usernames.kind.burn":`Сжигание`,"rating.pageTitle":`Рейтинг аккаунтов`,"rating.eyebrow":`Рейтинг / Лидерборд`,"rating.metricLoaded":`Загружено строк`,"rating.metricTopLevel":`Максимальный уровень`,"rating.metricAvgLevel":`Средний уровень`,"rating.metricPending":`С отложенными баллами`,"rating.searchPlaceholder":`Поиск по юзернейму, имени или ID`,"rating.minLevel":`Мин. уровень`,"rating.userID":`ID пользователя`,"rating.level":`Уровень`,"rating.stars":`Баллы`,"rating.progress":`Прогресс до следующего уровня`,"rating.pending":`Отложено`,"rating.computedAt":`Пересчитан`,"rating.levelValue":`Уровень {level}`,"rating.maxLevel":`Максимальный уровень`,"rating.progressHint":`До {target} осталось {remaining}`,"rating.loadingDetail":`Загружаем рейтинг аккаунта…`,"rating.detailTitle":`Рейтинг {user}`,"rating.detailEyebrow":`Рейтинг / Разбор по компонентам`,"rating.pendingBadge":`Отложено {amount}`,"rating.nextLevel":`Порог следующего уровня`,"rating.toNextLevel":`Баллов до следующего уровня`,"rating.breakdownTitle":`Из чего сложился рейтинг`,"rating.breakdownHint":`Вклад каждого источника: звёзды, активность, штрафы модерации и ручные корректировки.`,"rating.breakdownMismatch":`Сумма компонентов — {sum}, а сохранённый рейтинг — {total}. Запустите пересчёт, чтобы устранить расхождение.`,"rating.breakdownPending":`Компоненты уже учитывают {amount}, которые войдут в рейтинг только в указанную дату.`,"rating.currentLevelStars":`Порог текущего уровня`,"rating.nextLevelStars":`Порог следующего уровня`,"rating.pendingTitle":`Отложенные баллы`,"rating.pendingHint":`Баллы уже начислены, но войдут в рейтинг только в указанную дату.`,"rating.pendingDate":`Дата применения`,"rating.eventsTitle":`События рейтинга`,"rating.eventsHint":`Каждое изменение рейтинга с источником, исполнителем и причиной.`,"rating.eventKind":`Источник`,"rating.amount":`Изменение`,"rating.actionDock":`Операции с рейтингом`,"rating.openAccount":`Открыть аккаунт`,"rating.recompute":`Пересчитать`,"rating.recomputeHint":`Собирает рейтинг заново из звёзд, активности, штрафов и ручных корректировок.`,"rating.adjustTitle":`Ручная корректировка`,"rating.adjustAmount":`Значение (можно отрицательное)`,"rating.adjust":`Скорректировать`,"rating.adjustHint":`Значение прибавляется к ручной составляющей; отрицательное число уменьшает рейтинг.`,"rating.componentStars":`Звёзды`,"rating.componentStarsHint":`Купленные и полученные звёзды`,"rating.componentActivity":`Активность`,"rating.componentActivityHint":`Сообщения, сессии и долгосрочная вовлечённость`,"rating.componentPenalty":`Штрафы`,"rating.componentPenaltyHint":`Решения модерации и ограничения`,"rating.componentManual":`Ручные корректировки`,"rating.componentManualHint":`Правки, внесённые администраторами`,"rating.componentTotal":`Итоговый рейтинг`,"rating.kind.stars":`Звёзды`,"rating.kind.activity":`Активность`,"rating.kind.moderation":`Модерация`,"rating.kind.manual":`Вручную`,"rating.kind.recompute":`Пересчёт`,"route.verification":`Официальная верификация`,"route.verificationSubtitle":`Консоль / Верификация`,"layout.verification":`Верификация`,"permission.deniedTitle":`Недостаточно прав`,"permission.deniedEyebrow":`Консоль / Доступ`,"permission.deniedBody":`Этой сессии не выдано право {permission}, поэтому раздел закрыт.`,"permission.deniedHeading":`Раздел недоступен`,"permission.deniedHint":`Попросите добавить право в TELESRV_ADMIN_UI_PERMISSIONS и войдите заново.`,"verification.pageTitle":`Очередь заявок на верификацию`,"verification.eyebrow":`Верификация / Очередь`,"verification.searchPlaceholder":`ID заявки, ID цели, юзернейм или название`,"verification.statusAll":`Все статусы`,"verification.targetType":`Тип цели`,"verification.targetTypeAll":`Все типы`,"verification.reviewer":`Ревьюер`,"verification.reviewerPlaceholder":`Любой ревьюер`,"verification.target":`Цель`,"verification.applicant":`Заявитель`,"verification.category":`Категория`,"verification.submittedAt":`Подана`,"verification.alreadyVerified":`Бейдж уже стоит`,"verification.status.draft":`Черновик`,"verification.status.submitted":`Подана`,"verification.status.in_review":`На рассмотрении`,"verification.status.approved":`Одобрена`,"verification.status.rejected":`Отклонена`,"verification.status.cancelled":`Отменена`,"verification.type.bot":`Бот`,"verification.type.channel":`Канал`,"verification.type.supergroup":`Супергруппа`,"verification.type.user":`Пользователь`,"verification.loadingDetail":`Загружаем заявку…`,"verification.detailTitle":`Заявка №{id}`,"verification.detailEyebrow":`Верификация / Разбор заявки`,"verification.conflict":`Заявку уже изменил другой администратор. Данные перезагружены — проверьте статус и примите решение заново.`,"verification.controlsOk":`Права на цель подтверждены`,"verification.controlsLost":`Прав на цель больше нет`,"verification.controlsOkHint":`Заявитель управляет целью прямо сейчас — проверено по актуальным записям, а не по снимку на момент подачи.`,"verification.controlsLostHint":`Заявитель больше не управляет целью. Одобрить — значит выдать бейдж тому, кто уже не владеет пиром; обычно это причина отказать.`,"verification.targetSection":`Цель`,"verification.targetHint":`Пир, которому достанется бейдж, — в том виде, в каком он существует сейчас.`,"verification.openTarget":`Открыть цель`,"verification.targetTitle":`Название`,"verification.targetID":`ID пира`,"verification.applicantSection":`Заявитель`,"verification.applicantHint":`Кто подал заявку и сохранились ли у него права на цель.`,"verification.openApplicant":`Открыть аккаунт`,"verification.applicantID":`ID пользователя`,"verification.applicationSection":`Заявка`,"verification.applicationHint":`Всё, что заявитель прислал сам; выводится как обычный текст.`,"verification.correlationID":`Correlation ID`,"verification.createdAt":`Создана`,"verification.description":`Описание`,"verification.officialWebsite":`Официальный сайт`,"verification.socialLinks":`Соцсети`,"verification.pressLinks":`Публикации в СМИ`,"verification.additionalNote":`Комментарий заявителя`,"verification.notProvided":`Не указано`,"verification.linkSafetyHint":`Кликабельны только ссылки на http:// и https://, и открываются они в новой вкладке; всё остальное показано текстом.`,"verification.decisionSection":`Решение`,"verification.decisionHint":`Что решили, кто решил и с какой формулировкой.`,"verification.reviewedAt":`Решение принято`,"verification.version":`Версия (оптимистичная блокировка)`,"verification.decisionReason":`Причина решения`,"verification.noDecision":`Решения пока нет`,"verification.internalNote":`Внутренняя заметка`,"verification.adminOnly":`видно только администраторам`,"verification.eventsSection":`История`,"verification.eventsHint":`Неизменяемый след всех переходов статуса — с автором и причиной.`,"verification.eventKind":`Событие`,"verification.transition":`Было → стало`,"verification.eventNote":`Внутренняя заметка`,"verification.kind.created":`Создана`,"verification.kind.updated":`Изменена`,"verification.kind.submitted":`Подана`,"verification.kind.claimed":`Взята в работу`,"verification.kind.approved":`Одобрена`,"verification.kind.rejected":`Отклонена`,"verification.kind.cancelled":`Отменена`,"verification.kind.revoked":`Бейдж снят`,"verification.kind.notified":`Заявитель уведомлён`,"verification.actionDock":`Действия по заявке`,"verification.noActions":`В этом статусе действий нет.`,"verification.claim":`Взять в работу`,"verification.claimHint":`Закрепляет заявку за вами и переводит её в статус «на рассмотрении», чтобы двое не разбирали одно и то же.`,"verification.internalNotePlaceholder":`Заметка для других ревьюеров`,"verification.internalNoteHint":`Необязательно. Сохраняется вместе с решением и видно только администраторам — заявителю не уходит.`,"verification.alreadyVerifiedHint":`Бейдж на цели уже стоит: одобрение лишь зафиксирует решение.`,"verification.approve":`Одобрить`,"verification.approveHint":`Выдаёт цели официальный бейдж и закрывает заявку.`,"verification.reject":`Отклонить`,"verification.rejectHint":`Причина обязательна: именно эту формулировку увидит заявитель, поэтому напишите, чего не хватило.`,"verification.dangerZone":`Опасная зона`,"verification.revoke":`Снять верификацию`,"verification.revokeHint":`Убирает бейдж с цели. Одобренная заявка остаётся в истории.`,"verification.revokeNotVerified":`Бейджа на цели сейчас нет — снимать нечего.`,"route.botVerification":`Сторонняя верификация`,"route.botVerificationSubtitle":`Консоль / Сторонняя верификация`,"layout.botVerification":`Сторонние метки`,"picker.system":`Системный`,"picker.botPlaceholder":`Юзернейм бота или ID`,"botverification.pageTitle":`Сторонняя верификация`,"botverification.eyebrow":`Сторонняя верификация / Верификаторы, иконки, метки`,"botverification.explainTitle":`Это иконка компании-верификатора, а не официальная галочка`,"botverification.explainText":`Сторонняя метка — это собственная иконка бота-верификатора, которая рисуется ПЕРЕД именем аккаунта, бота или канала, плюс одна строка описания в профиле. Она значит только одно: «этот верификатор поручился за этот аккаунт».`,"botverification.explainIcon":`Иконка — это документ кастомного эмодзи. Клиент забирает его через messages.getCustomEmojiDocuments, поэтому ID, за которым нет реального документа, выглядит как полное отсутствие метки. Именно поэтому метки выдаются из каталога ниже, а не из набранного руками числа.`,"botverification.explainOfficial":`Официальная галочка — другой механизм, её выдаёт платформа в разделе «Официальная верификация». Они хранятся, показываются и снимаются по отдельности, и одна не подразумевает другую.`,"botverification.openOfficial":`Официальная верификация`,"botverification.manageMissing":`Эта сессия может читать раздел и решать заявки, но не может менять верификаторов и каталог иконок — для этого нужно право botverification.manage.`,"botverification.tabRequests":`Заявки`,"botverification.tabVerifiers":`Верификаторы`,"botverification.tabIcons":`Каталог иконок`,"botverification.tabMarks":`Выданные метки`,"botverification.queueTitle":`Очередь заявок`,"botverification.queueHint":`Заявки, поданные боту-верификатору владельцем аккаунта или канала. Счётчики считают всю очередь, а не страницу ниже.`,"botverification.searchPlaceholder":`ID заявки, ID цели, юзернейм или название`,"botverification.statusAll":`Все статусы`,"botverification.status.pending":`Ожидает решения`,"botverification.status.approved":`Одобрена`,"botverification.status.rejected":`Отклонена`,"botverification.status.revoked":`Метка снята`,"botverification.peer.user":`Аккаунт`,"botverification.peer.channel":`Канал`,"botverification.peerType":`Тип цели`,"botverification.peerTypeAll":`Все типы`,"botverification.verifier":`Верификатор`,"botverification.verifierAll":`Все верификаторы`,"botverification.verifierID":`ID бота-верификатора`,"botverification.applicant":`Заявитель`,"botverification.applicantID":`ID пользователя`,"botverification.target":`Цель`,"botverification.targetTitle":`Название`,"botverification.targetID":`ID цели`,"botverification.reason":`Обоснование`,"botverification.requestedDescription":`Запрошенное описание`,"botverification.description":`Описание`,"botverification.createdAt":`Подана`,"botverification.company":`Компания`,"botverification.companyPlaceholder":`ООО «Ромашка Верификация»`,"botverification.bot":`Бот`,"botverification.icon":`Иконка`,"botverification.iconDocument":`ID документа`,"botverification.iconName":`Название`,"botverification.markCount":`Метки`,"botverification.grantedBy":`Кто выдал`,"botverification.notProvided":`Не задано`,"botverification.verifiersTitle":`Боты-верификаторы`,"botverification.verifiersHint":`Боты, которым разрешено выдавать свою метку. Статус верификатора выдаётся вручную на уровне сервера, так что каждая строка здесь — это печатный станок бейджей, включённый оператором.`,"botverification.grantTitle":`Выдать статус верификатора`,"botverification.updateTitle":`Обновить верификатора`,"botverification.grantHint":`Бот получает иконку из каталога и название компании, от имени которой он поручается. Тот же вызов обновляет существующего верификатора — поэтому в нём есть version.`,"botverification.grantBot":`Бот`,"botverification.grantIcon":`Иконка из каталога`,"botverification.grantIconPick":`Выберите иконку`,"botverification.defaultDescription":`Описание по умолчанию`,"botverification.defaultDescriptionPlaceholder":`Проверено компанией «Ромашка»`,"botverification.canModify":`Верификатор может задавать своё описание для каждой цели`,"botverification.canModifyShort":`Своё описание`,"botverification.canModifyHint":`Это botVerifierSettings.can_modify_custom_description: если выключено, любая метка этого верификатора несёт описание по умолчанию — что бы ни просил заявитель.`,"botverification.noActiveIcons":`В каталоге нет активных иконок, выдавать нечего. Сначала добавьте иконку в каталог.`,"botverification.grantNote":`Бот сможет ставить метки сразу, как только строка появится и будет включена.`,"botverification.grant":`Выдать статус`,"botverification.update":`Обновить верификатора`,"botverification.editing":`Обновляем {bot} — версия {version} уходит как оптимистичная блокировка: если строку успел изменить кто-то другой, запрос отклонят, а не перезапишут.`,"botverification.cancelEdit":`Отменить обновление`,"botverification.edit":`Изменить`,"botverification.enable":`Включить`,"botverification.disable":`Отключить`,"botverification.enabled":`Включён`,"botverification.disabled":`отключён`,"botverification.disableHint":`Отключение — это рубильник для одного верификатора: уже выданные метки продолжают показываться, но новых бот поставить не может, и его настройки перестают уезжать в botInfo.`,"botverification.revokeVerifier":`Отозвать статус`,"botverification.revokeVerifierHint":`Отзыв статуса удаляет строку и все метки, которые этот верификатор выдал: иконка исчезнет сразу у всех его целей.`,"botverification.iconsTitle":`Каталог иконок`,"botverification.iconsHint":`Документы кастомных эмодзи, которыми верификатор может помечать цели. Ничего другого иконкой быть не может, поэтому неверный бейдж отсекается здесь, а не исправляется потом.`,"botverification.addIconTitle":`Добавить иконку или переименовать`,"botverification.addIconHint":`ID документа должен указывать на реальный документ кастомного эмодзи на этом сервере; раздел «Эмодзи» показывает их вместе с ID. Повторное добавление того же ID переименует запись, а не создаст вторую.`,"botverification.iconNamePlaceholder":`Синяя галочка «Ромашки»`,"botverification.iconOwner":`Владелец`,"botverification.iconOwnerShared":`Общая`,"botverification.iconOwnerHint":`Общую иконку можно выдать любому верификатору; если указать владельца, она закрепится только за этим ботом.`,"botverification.iconDocumentHint":`ID, за которым нет документа, даёт невидимый бейдж: в базе цель помечена, а клиент не рисует ничего.`,"botverification.addIconNote":`Добавление иконки само по себе ничего не выдаёт — оно лишь делает документ доступным для выдачи.`,"botverification.addIcon":`Сохранить иконку`,"botverification.iconActive":`Активна`,"botverification.iconInactive":`Выведена`,"botverification.usedBy":`Используют верификаторов`,"botverification.activateIcon":`Активировать`,"botverification.deactivateIcon":`Вывести`,"botverification.deactivateIconHint":`Выведенную иконку больше нельзя выдать новым верификаторам. У уже выданных меток она остаётся: иконка копируется в метку в момент выдачи.`,"botverification.marksTitle":`Выданные метки`,"botverification.marksHint":`Все цели, которые сейчас несут стороннюю метку, кем бы она ни была выдана: решением оператора, самим ботом-верификатором или владельцем цели через bots.setCustomVerification.`,"botverification.markSearchPlaceholder":`ID цели, юзернейм или название`,"botverification.revokeMark":`Снять метку`,"botverification.revokeMarkHint":`Снятие метки убирает с цели иконку и описание. Заявка, из которой метка появилась, остаётся в истории.`,"botverification.loadingDetail":`Загружаем заявку…`,"botverification.detailTitle":`Заявка #{id}`,"botverification.detailEyebrow":`Сторонняя верификация / Разбор`,"botverification.conflict":`Заявку уже изменил другой админ. Данные перезагружены — посмотрите статус перед новым решением.`,"botverification.markActive":`Метка стоит`,"botverification.markInactive":`Метки на цели нет`,"botverification.markActiveHint":`На этой цели метка этого верификатора уже стоит; одобрение обновит описание и зафиксирует решение.`,"botverification.verifierSection":`Верификатор`,"botverification.verifierHint":`Компания, чью иконку получит цель, — в том виде, в каком её строка выглядит сейчас.`,"botverification.openVerifier":`Открыть бота-верификатора`,"botverification.verifierMissing":`Строки верификатора больше нет: его статус отозвали уже после подачи заявки. Выдавать нечего, поэтому заявку остаётся только отклонить.`,"botverification.verifierDisabledHint":`Верификатор отключён. Пока оператор не включит его снова, новые метки он ставить не может.`,"botverification.targetSection":`Цель`,"botverification.targetHint":`Аккаунт, бот или канал, к которому будет прикреплена иконка.`,"botverification.openTarget":`Открыть цель`,"botverification.applicantSection":`Заявитель`,"botverification.applicantHint":`Кто подал заявку боту-верификатору.`,"botverification.openApplicant":`Открыть аккаунт`,"botverification.requestSection":`Заявка`,"botverification.requestHint":`Что написал заявитель — как обычный текст.`,"botverification.correlationID":`Correlation ID`,"botverification.markPreview":`Описание, которое получит метка`,"botverification.markPreviewHint":`Считается так же, как на бэкенде: формулировка заявителя берётся только если верификатору разрешено своё описание, иначе применяется описание верификатора по умолчанию.`,"botverification.descriptionIgnoredHint":`Этому верификатору нельзя задавать описание для отдельной цели, поэтому запрошенный текст игнорируется и применяется описание по умолчанию.`,"botverification.decisionSection":`Решение`,"botverification.decisionHint":`Кто и что решил и какими словами.`,"botverification.decidedBy":`Решение принял`,"botverification.approvedAt":`Одобрена`,"botverification.rejectedAt":`Отклонена`,"botverification.version":`Версия (оптимистичная блокировка)`,"botverification.decisionReason":`Причина решения`,"botverification.noDecision":`Решения пока нет`,"botverification.internalNote":`Внутренняя заметка`,"botverification.adminOnly":`только для админов`,"botverification.internalNotePlaceholder":`Заметка для других админов`,"botverification.internalNoteHint":`Необязательно. Хранится вместе с решением и видна только админам — заявителю не отправляется.`,"botverification.actionDock":`Решение по заявке`,"botverification.noActions":`В этом статусе действий нет.`,"botverification.approve":`Одобрить`,"botverification.approveHint":`Ставит иконку верификатора перед именем цели, описание — в профиль, и уведомляет заявителя.`,"botverification.reject":`Отклонить`,"botverification.rejectHint":`Причина обязательна: именно эту формулировку увидит заявитель, поэтому напишите, чего именно не хватило.`,"botverification.dangerZone":`Опасная зона`,"botverification.revokeRequest":`Снять метку`,"botverification.revokeRequestHint":`Убирает с цели иконку и описание и закрывает заявку как «метка снята». Официальная галочка, если она есть, не затрагивается.`,"botverification.revokeNoMark":`Метки на цели сейчас нет — снятие только закроет заявку.`,"botverification.rosterDenied":`Сервер не отдал этой сессии список верификаторов и каталог иконок (403), поэтому оба списка здесь пустые — заявки разбирать всё равно можно.`}},ot=(0,g.createContext)(null);function st({children:e}){let[t,n]=(0,g.useState)(()=>ut());(0,g.useEffect)(()=>{try{localStorage.setItem(it,t)}catch{}let e=t===`zh`?`zh-CN`:t===`ru`?`ru`:`en`;document.documentElement.lang=e,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=lt(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>lt(t,e,n)}),[t]);return(0,H.jsx)(ot.Provider,{value:r,children:e})}function U(){let e=(0,g.useContext)(ot);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function ct(){let{lang:e,setLang:t,t:n}=U();return(0,H.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`,`ru`].map(r=>(0,H.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function lt(e,t,n){let r=at[e][t]??at.en[t]??t;return n?r.replace(/\{(\w+)\}/g,(e,t)=>String(n[t]??``)):r}function ut(){try{let e=dt(new URLSearchParams(window.location.search).get(`lang`));if(e)return e}catch{}try{let e=dt(localStorage.getItem(it));if(e)return e}catch{}let e=navigator.languages?.length?navigator.languages:[navigator.language];for(let t of e){let e=dt(t);if(e)return e}return`en`}function dt(e){if(!e)return null;let t=e.trim().toLowerCase().replace(`_`,`-`);return t===`zh`||t.startsWith(`zh-`)?`zh`:t===`en`||t.startsWith(`en-`)?`en`:t===`ru`||t.startsWith(`ru-`)?`ru`:null}function ft(e){let t=e.trim();return!t||t.startsWith(`+`)?t:/^\d+$/.test(t)?`+${t}`:t}function W(e){let t=e.trim();return t?t.startsWith(`@`)?t:`@${t}`:``}function pt(e){return`${e.FirstName||``} ${e.LastName||``}`.trim()||`-`}function mt(e,t){let n=t??(e=>({"channel.kind.broadcast":`Channel`,"channel.kind.forum":`Supergroup / Forum`,"channel.kind.megagroup":`Supergroup`,"channel.kind.generic":`Channel / Group`})[e]??e);return e.Broadcast&&!e.Megagroup?n(`channel.kind.broadcast`):e.Megagroup&&e.Forum?n(`channel.kind.forum`):e.Megagroup?n(`channel.kind.megagroup`):n(`channel.kind.generic`)}function G(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function ht(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function gt(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 _t(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function vt(e){let t=(e??``).trim();if(!t)return 0;let n=Number(t);return Number.isFinite(n)?n:0}function yt(e){let t=(e??``).trim();if(!t)return`0`;let n=Number(t);return Number.isFinite(n)?n.toLocaleString():t}var bt={XTR:0,TON:9,USD:2,EUR:2,RUB:2};function xt(e){let t=(e??``).trim().toUpperCase();return t in bt?bt[t]:2}function St(e,t){let n=(e??``).trim();if(!n)return`0`;if(!/^-?\d+$/.test(n))return n;let r=xt(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}${Ct(o)}.${s}`:`${c}${Ct(o)}`}function Ct(e){return e.replace(/\B(?=(\d{3})+(?!\d))/g,` `)}function wt(e,t){let n=(t??``).trim().toUpperCase(),r=St(e,n);return n?`${r} ${n}`:r}function Tt(e,t){let n=(e??``).trim().replace(/\s+/g,``).replace(`,`,`.`);if(!n)return`0`;if(!/^\d*(\.\d*)?$/.test(n)||n===`.`)return null;let r=xt(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 Et(e){let t=(e??``).trim();if(!t)return`0`;let n=Number(t);return Number.isFinite(n)?n>0?`+${n.toLocaleString()}`:n.toLocaleString():t}function Dt(e,t=`msg ids invalid`){let n=e.split(/[\s,]+/).map(e=>e.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}function K({title:e,eyebrow:t,children:n,actions:r}){return(0,H.jsxs)(`div`,{className:`page-frame`,children:[(0,H.jsxs)(`div`,{className:`page-title-row`,children:[(0,H.jsxs)(`div`,{children:[t&&(0,H.jsx)(`div`,{className:`eyebrow`,children:t}),(0,H.jsx)(`h2`,{children:e})]}),r&&(0,H.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Ot({children:e}){return(0,H.jsx)(`div`,{className:`query-panel`,children:e})}function kt({main:e,side:t}){return(0,H.jsxs)(`div`,{className:`split-layout`,children:[(0,H.jsx)(`div`,{className:`split-main`,children:e}),(0,H.jsx)(`aside`,{className:`split-side`,children:t})]})}function q({title:e,text:t,action:n}){return(0,H.jsxs)(`div`,{className:`section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`h2`,{children:e}),t&&(0,H.jsx)(`p`,{children:t})]}),n&&(0,H.jsx)(`div`,{className:`section-action`,children:n})]})}function J({children:e}){return(0,H.jsxs)(`div`,{className:`alert`,children:[(0,H.jsx)(ee,{size:16}),` `,(0,H.jsx)(`span`,{children:e})]})}function Y({children:e,tone:t=`neutral`}){return(0,H.jsx)(`span`,{className:`badge ${t}`,children:e})}function At({label:e,value:t,tone:n}){return(0,H.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{children:t})]})}function X({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,H.jsxs)(`div`,{className:`metric ${n}`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function Z({label:e,value:t,mono:n=!1}){return(0,H.jsxs)(`div`,{className:`summary-item`,children:[(0,H.jsx)(`span`,{children:e}),(0,H.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function jt({rows:e}){let{t}=U();return(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`audit.id`)}),(0,H.jsx)(`th`,{children:t(`audit.commandID`)}),(0,H.jsx)(`th`,{children:t(`audit.action`)}),(0,H.jsx)(`th`,{children:t(`audit.actor`)}),(0,H.jsx)(`th`,{children:t(`audit.status`)}),(0,H.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,H.jsx)(`th`,{children:t(`audit.reason`)}),(0,H.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[e.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.ID}),(0,H.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,H.jsx)(`td`,{children:e.Action}),(0,H.jsx)(`td`,{children:e.Actor}),(0,H.jsx)(`td`,{children:e.Status}),(0,H.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,H.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,H.jsx)(`td`,{children:G(e.CreatedAt)})]},e.ID)),e.length===0&&(0,H.jsx)(Mt,{colSpan:8})]})]})})}function Mt({colSpan:e}){let{t}=U();return(0,H.jsx)(`tr`,{children:(0,H.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function Nt({label:e}){return(0,H.jsx)(`section`,{className:`surface`,children:(0,H.jsx)(`div`,{className:`loading-line`,children:e})})}function Pt({value:e}){return(0,H.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function Ft({username:e,collectibles:t}){let{t:n}=U(),r=W(e??``),i=t??[];return i.length===0?(0,H.jsx)(H.Fragment,{children:r||`-`}):(0,H.jsxs)(H.Fragment,{children:[r,(0,H.jsx)(`ul`,{className:`username-branch`,children:i.map(e=>(0,H.jsxs)(`li`,{className:e.Active?``:`inactive`,children:[(0,H.jsx)(`span`,{children:W(e.Username)}),!e.Active&&(0,H.jsx)(`em`,{children:n(`usernames.inactive`)})]},e.Username))})]})}var It=`verification.review`,Lt=`botverification.review`,Rt=`botverification.manage`,zt=(0,g.createContext)([]);function Bt({permissions:e,children:t}){return(0,H.jsx)(zt.Provider,{value:e,children:t})}function Vt(){let e=(0,g.useContext)(zt);return(0,g.useMemo)(()=>({permissions:e,can:t=>e.includes(`*`)||e.includes(t)}),[e])}function Ht(e){return Vt().can(e)}function Ut({permission:e,children:t}){let{can:n}=Vt();return n(e)?(0,H.jsx)(H.Fragment,{children:t}):(0,H.jsx)(Wt,{permission:e})}function Wt({permission:e}){let{t}=U();return(0,H.jsxs)(K,{title:t(`permission.deniedTitle`),eyebrow:t(`permission.deniedEyebrow`),children:[(0,H.jsx)(J,{children:t(`permission.deniedBody`,{permission:e})}),(0,H.jsx)(`section`,{className:`section-block`,children:(0,H.jsx)(`div`,{className:`entity-head`,children:(0,H.jsxs)(`div`,{children:[(0,H.jsxs)(`div`,{className:`entity-title`,children:[(0,H.jsx)(He,{size:16}),` `,t(`permission.deniedHeading`)]}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:t(`permission.deniedHint`)})]})})})]})}function Gt(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function Kt(e,t){return e.startsWith(`/bot-verification`)?t(`route.botVerification`):e.startsWith(`/verification`)?t(`route.verification`):e.startsWith(`/collectible-usernames`)?t(`route.collectibleUsernames`):e.startsWith(`/account-ratings`)?t(`route.accountRatings`):e.startsWith(`/accounts`)?t(`route.accounts`):e.startsWith(`/channels`)?t(`route.channels`):e.startsWith(`/bots`)?t(`route.bots`):e.startsWith(`/moderation`)?t(`route.moderation`):e.startsWith(`/emoji`)?t(`route.emoji`):e.startsWith(`/messages`)?t(`route.messages`):e.startsWith(`/give-gifts`)?t(`route.giveGifts`):e.startsWith(`/gifts`)?t(`route.gifts`):t(`route.dashboard`)}function qt(e,t){return e.startsWith(`/bot-verification`)?t(`route.botVerificationSubtitle`):e.startsWith(`/verification`)?t(`route.verificationSubtitle`):e.startsWith(`/collectible-usernames`)?t(`route.collectibleUsernamesSubtitle`):e.startsWith(`/account-ratings`)?t(`route.accountRatingsSubtitle`):e.startsWith(`/accounts`)?t(`route.accountsSubtitle`):e.startsWith(`/channels`)?t(`route.channelsSubtitle`):e.startsWith(`/bots`)?t(`route.botsSubtitle`):e.startsWith(`/moderation`)?t(`route.moderationSubtitle`):e.startsWith(`/emoji`)?t(`route.emojiSubtitle`):e.startsWith(`/messages`)?t(`route.messagesSubtitle`):e.startsWith(`/give-gifts`)?t(`route.giveGiftsSubtitle`):e.startsWith(`/gifts`)?t(`route.giftsSubtitle`):t(`route.dashboardSubtitle`)}var Jt=`telesrv.admin.theme`,Yt=(0,g.createContext)(null);function Xt(e){document.documentElement.setAttribute(`data-theme`,e),document.documentElement.style.colorScheme=e}function Zt({children:e}){let[t,n]=(0,g.useState)(()=>en());(0,g.useEffect)(()=>{Xt(t);try{localStorage.setItem(Jt,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(Jt)}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,H.jsx)(Yt.Provider,{value:a,children:e})}function Qt(){let e=(0,g.useContext)(Yt);if(!e)throw Error(`useTheme must be used inside ThemeProvider`);return e}function $t(){let{theme:e,toggleTheme:t}=Qt(),{t:n}=U(),r=n(e===`light`?`theme.switchToDark`:`theme.switchToLight`);return(0,H.jsx)(`button`,{className:`theme-toggle`,type:`button`,onClick:t,"aria-label":r,title:r,children:e===`dark`?(0,H.jsx)(Je,{size:16}):(0,H.jsx)(Oe,{size:16})})}function en(){try{let e=localStorage.getItem(Jt);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 tn({href:e,navigate:t,className:n,children:r}){return(0,H.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function nn(){let{t:e}=U();return(0,H.jsxs)(`div`,{className:`boot-screen`,children:[(0,H.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,H.jsx)(`div`,{className:`loader-bar`})]})}function rn({actor:e,route:t,navigate:n,onLogout:r,children:i}){let{t:a}=U(),o=Ht(It),s=Ht(Lt),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,H.jsxs)(`div`,{className:`shell`,children:[(0,H.jsxs)(`aside`,{className:`sidebar`,children:[(0,H.jsxs)(tn,{className:`brand`,href:`/`,navigate:n,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,H.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,H.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,H.jsx)(an,{icon:(0,H.jsx)(we,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,H.jsx)(an,{icon:(0,H.jsx)(et,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,H.jsx)(an,{icon:(0,H.jsx)(Ve,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,H.jsx)(an,{icon:(0,H.jsx)(ce,{size:16}),href:`/bots`,route:t,navigate:n,children:a(`layout.bots`)}),(0,H.jsx)(an,{icon:(0,H.jsx)(Be,{size:16}),href:`/moderation`,route:t,navigate:n,children:a(`layout.moderation`)}),o&&(0,H.jsx)(an,{icon:(0,H.jsx)(F,{size:16}),href:`/verification`,route:t,navigate:n,children:a(`layout.verification`)}),s&&(0,H.jsx)(an,{icon:(0,H.jsx)(Ge,{size:16}),href:`/bot-verification`,route:t,navigate:n,children:a(`layout.botVerification`)}),(0,H.jsx)(an,{icon:(0,H.jsx)(oe,{size:16}),href:`/collectible-usernames`,route:t,navigate:n,children:a(`layout.collectibleUsernames`)}),(0,H.jsx)(an,{icon:(0,H.jsx)(Xe,{size:16}),href:`/account-ratings`,route:t,navigate:n,children:a(`layout.accountRatings`)}),(0,H.jsx)(an,{icon:(0,H.jsx)(xe,{size:16}),href:`/gifts`,route:t,navigate:n,children:a(`layout.gifts`)}),(0,H.jsx)(an,{icon:(0,H.jsx)(Le,{size:16}),href:`/give-gifts`,route:t,navigate:n,children:a(`layout.giveGifts`)}),(0,H.jsx)(an,{icon:(0,H.jsx)(We,{size:16}),href:`/emoji`,route:t,navigate:n,children:a(`layout.emoji`)}),(0,H.jsxs)(`div`,{className:`nav-section ${c?`active`:``} ${l?`open`:``}`,children:[(0,H.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":l,onClick:()=>u(e=>!e),children:[(0,H.jsx)(De,{size:16}),(0,H.jsx)(`span`,{children:a(`layout.messages`)}),(0,H.jsx)(fe,{className:`nav-section-chevron`,size:15})]}),l&&(0,H.jsxs)(`div`,{className:`nav-children`,children:[(0,H.jsx)(an,{href:`/messages/private`,route:t,navigate:n,activeWhen:e=>e===`/messages`||e===`/messages/detail`||e.startsWith(`/messages/private`),children:a(`layout.privateMessages`)}),(0,H.jsx)(an,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,H.jsxs)(`div`,{className:`sidebar-status`,children:[(0,H.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(Re,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,H.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(he,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,H.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,H.jsxs)(`div`,{className:`runtime-row`,children:[(0,H.jsx)(Ue,{size:14}),(0,H.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,H.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,H.jsxs)(`div`,{className:`workspace`,children:[(0,H.jsxs)(`header`,{className:`topbar`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:qt(t.path,a)}),(0,H.jsx)(`h1`,{children:Kt(t.path,a)})]}),(0,H.jsxs)(`div`,{className:`topbar-actions`,children:[(0,H.jsx)($t,{}),(0,H.jsx)(ct,{}),(0,H.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,H.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:d,title:a(`layout.logout`),children:[(0,H.jsx)(Ee,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,H.jsx)(`main`,{className:`content`,children:i})]})]})}function an({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,H.jsxs)(tn,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,H.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,H.jsx)(`span`,{children:i})]})}function on({onLogin:e}){let{t}=U(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(!1);async function c(t){t.preventDefault(),s(!0),a(``);try{let t=await k.login(n);e({actor:t.actor,permissions:t.permissions??[]})}catch(e){a(O(e))}finally{s(!1)}}return(0,H.jsx)(`main`,{className:`login-page`,children:(0,H.jsxs)(`section`,{className:`login-panel`,children:[(0,H.jsxs)(`div`,{className:`login-head`,children:[(0,H.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,H.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,H.jsxs)(`span`,{children:[(0,H.jsx)(`strong`,{children:`telesrv`}),(0,H.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,H.jsxs)(`div`,{className:`login-head-actions`,children:[(0,H.jsx)($t,{}),(0,H.jsx)(ct,{}),(0,H.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,H.jsxs)(`div`,{className:`login-copy`,children:[(0,H.jsx)(`h1`,{children:t(`login.heading`)}),(0,H.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,H.jsx)(J,{children:i}),(0,H.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:t(`login.secret`)}),(0,H.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,H.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var sn=m();function Q({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,disabled:o=!1,onDone:s,onError:c}){let{t:l}=U(),[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);function x(){p(``),h(null),v(``)}async function S(e){if(!f.trim()){v(l(`action.reasonRequired`));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 C=m?.dry_run&&!m.error,w=`btn ${a===`danger`?`danger`:a===`warn`?`warn`:``} ${i?`compact-btn`:``}`,T=(0,g.useMemo)(()=>{try{return n()}catch(e){return{payload_error:O(e)}}},[u,n]);return(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:w,type:`button`,disabled:o,onClick:()=>{x(),d(!0)},children:[r,e]}),u&&(0,sn.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:l(`action.flow`)}),(0,H.jsx)(`h2`,{children:e})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>d(!1),"aria-label":l(`action.close`),children:(0,H.jsx)(nt,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body`,children:[(0,H.jsxs)(`div`,{className:`command-steps`,children:[(0,H.jsxs)(`div`,{className:`command-step ${f.trim()?`done`:`active`}`,children:[(0,H.jsx)(`span`,{children:`1`}),(0,H.jsx)(`strong`,{children:l(`action.stepReason`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${m?.dry_run?`done`:f.trim()?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`2`}),(0,H.jsx)(`strong`,{children:l(`action.stepDryRun`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${m&&!m.dry_run&&!m.error?`done`:C?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`3`}),(0,H.jsx)(`strong`,{children:l(`action.stepConfirm`)})]})]}),(0,H.jsxs)(`label`,{className:`form-field`,children:[(0,H.jsx)(`span`,{children:l(`action.reason`)}),(0,H.jsx)(`textarea`,{value:f,onChange:e=>p(e.target.value),rows:3,placeholder:l(`action.reasonPlaceholder`)})]}),(0,H.jsxs)(`div`,{className:`command-preview`,children:[(0,H.jsxs)(`div`,{className:`preview-head`,children:[(0,H.jsx)(ve,{size:14}),` `,l(`action.requestPreview`)]}),(0,H.jsx)(Pt,{value:JSON.stringify(T,null,2)})]}),_&&(0,H.jsx)(J,{children:_}),m&&(0,H.jsxs)(`div`,{className:`result-box`,children:[(0,H.jsxs)(`div`,{className:`result-title`,children:[m.error?(0,H.jsx)(ee,{size:16}):(0,H.jsx)(I,{size:16}),(0,H.jsx)(`strong`,{children:m.message||m.error||l(`action.result`)})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:l(`action.commandID`)}),(0,H.jsx)(`strong`,{children:m.command_id})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:l(`action.status`)}),(0,H.jsx)(`strong`,{children:m.status})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:l(`action.dryRun`)}),(0,H.jsx)(`strong`,{children:m.dry_run?l(`common.yes`):l(`common.no`)})]}),(0,H.jsx)(`div`,{className:`result-message`,children:m.message||m.error}),m.details&&(0,H.jsx)(Pt,{value:JSON.stringify(m.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>d(!1),children:l(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:y,children:[y?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(je,{size:15}),l(m?`action.runAgain`:`action.runDry`)]}),(0,H.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>S(!0),disabled:y||!C,children:[(0,H.jsx)(I,{size:15}),l(`action.confirm`)]})]})]})}),document.body)]})}function cn({rows:e,userID:t,onDone:n}){let{t:r}=U(),[i,a]=(0,g.useState)(()=>new Set);(0,g.useEffect)(()=>{a(new Set)},[t]);let o=(0,g.useMemo)(()=>e.filter(e=>!i.has(e.Hash)),[e,i]);function s(e){a(t=>e(t)),n()}return(0,H.jsxs)(`div`,{className:`authorization-block`,children:[(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:r(`auth.device`)}),(0,H.jsx)(`th`,{children:r(`auth.platform`)}),(0,H.jsx)(`th`,{children:r(`auth.ip`)}),(0,H.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,H.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,H.jsxs)(`tbody`,{children:[o.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,H.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,H.jsx)(`td`,{children:n.IP}),(0,H.jsx)(`td`,{children:G(n.ActiveAt)}),(0,H.jsx)(`td`,{className:`device-actions-cell`,children:(0,H.jsxs)(`div`,{className:`device-actions`,children:[(0,H.jsx)(Q,{label:r(`auth.revokeCurrent`),icon:(0,H.jsx)(Ee,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,hash:n.Hash}),onDone:()=>s(e=>new Set([...e,n.Hash]))}),(0,H.jsx)(Q,{label:r(`auth.keepCurrent`),icon:(0,H.jsx)(Ve,{size:13}),compact:!0,path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,keep_hash:n.Hash}),onDone:()=>s(()=>new Set(e.filter(e=>e.Hash!==n.Hash).map(e=>e.Hash)))})]})})]},n.Hash)),o.length===0&&(0,H.jsx)(Mt,{colSpan:5})]})]})}),(0,H.jsx)(`div`,{className:`danger-zone`,children:(0,H.jsx)(Q,{label:r(`auth.revokeAll`),icon:(0,H.jsx)(ue,{size:15}),path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,revoke_all:!0}),onDone:()=>s(()=>new Set(e.map(e=>e.Hash)))})})]})}function ln({scam:e,fake:t}){let{t:n}=U();return!e&&!t?null:(0,H.jsxs)(H.Fragment,{children:[e&&(0,H.jsx)(Y,{tone:`danger`,children:n(`flags.scam`)}),t&&(0,H.jsx)(Y,{tone:`danger`,children:n(`flags.fake`)})]})}function un({idKey:e,id:t,path:n,scam:r,fake:i,onDone:a}){let{t:o}=U();return(0,H.jsxs)(`div`,{className:`action-stack`,children:[(0,H.jsx)(Q,{label:o(r?`flags.clearScam`:`flags.setScam`),icon:(0,H.jsx)(Be,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,scam:!r,fake:r?i:!1}),onDone:a}),(0,H.jsx)(Q,{label:o(i?`flags.clearFake`:`flags.setFake`),icon:(0,H.jsx)(ne,{size:15}),tone:`danger`,path:n,payload:()=>({[e]:t,fake:!i,scam:i?r:!1}),onDone:a})]})}function dn({id:e,support:t,onDone:n}){let{t:r}=U();return(0,H.jsx)(Q,{label:r(t?`attr.clearSupport`:`attr.setSupport`),icon:(0,H.jsx)(Te,{size:15}),tone:`neutral`,path:`/api/actions/set-support`,payload:()=>({user_id:e,support:!t}),onDone:n})}function fn({idKey:e,id:t,path:n,current:r,onDone:i}){let{t:a}=U(),[o,s]=(0,g.useState)(r.replace(/^@/,``));return(0,H.jsxs)(`div`,{className:`attr-block`,children:[(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:a(`attr.username`)}),(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:`username`})]}),(0,H.jsx)(Q,{label:a(`attr.setUsername`),icon:(0,H.jsx)(oe,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,username:o.trim().replace(/^@/,``)}),onDone:i})]})}function pn({idKey:e,id:t,path:n,onDone:r}){let{t:i}=U(),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(!0),[l,u]=(0,g.useState)(`0`),[d,f]=(0,g.useState)(``);return(0,H.jsxs)(`div`,{className:`attr-block`,children:[(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:a,onChange:e=>o(e.target.checked)}),` `,i(`attr.forProfile`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:s,onChange:e=>c(e.target.checked)}),` `,i(`attr.hasColor`)]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:i(`attr.colorIndex`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,max:`20`,value:l,onChange:e=>u(e.target.value)})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:i(`attr.bgEmojiID`)}),(0,H.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),placeholder:`0`})]}),(0,H.jsx)(Q,{label:i(`attr.setColor`),icon:(0,H.jsx)(ke,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,for_profile:a,has_color:s,color:_t(l),background_emoji_id:d.trim()||`0`}),onDone:r})]})}function mn({idKey:e,id:t,path:n,onDone:r}){let{t:i}=U(),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(`0`);return(0,H.jsxs)(`div`,{className:`attr-block`,children:[(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:i(`attr.emojiDocID`)}),(0,H.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),placeholder:`0 = clear`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:i(`attr.emojiUntil`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:s,onChange:e=>c(e.target.value)})]}),(0,H.jsx)(Q,{label:i(`attr.setEmojiStatus`),icon:(0,H.jsx)(We,{size:15}),tone:`neutral`,path:n,payload:()=>({[e]:t,document_id:a.trim()||`0`,until:_t(s)}),onDone:r})]})}function hn({channel:e,onDone:t}){let{t:n}=U(),[r,i]=(0,g.useState)(e.Gigagroup),[a,o]=(0,g.useState)(e.AntiSpam),[s,c]=(0,g.useState)(e.ParticipantsHidden),[l,u]=(0,g.useState)(e.NoForwards),[d,f]=(0,g.useState)(e.JoinToSend),[p,m]=(0,g.useState)(e.JoinRequest),[h,_]=(0,g.useState)(String(e.SlowmodeSeconds));(0,g.useEffect)(()=>{i(e.Gigagroup),o(e.AntiSpam),c(e.ParticipantsHidden),u(e.NoForwards),f(e.JoinToSend),m(e.JoinRequest),_(String(e.SlowmodeSeconds))},[e]);function v(){let t={channel_id:e.ID};return r!==e.Gigagroup&&(t.gigagroup=r),a!==e.AntiSpam&&(t.antispam=a),s!==e.ParticipantsHidden&&(t.participants_hidden=s),l!==e.NoForwards&&(t.noforwards=l),d!==e.JoinToSend&&(t.join_to_send=d),p!==e.JoinRequest&&(t.join_request=p),_t(h)!==e.SlowmodeSeconds&&(t.slowmode_seconds=_t(h)),t}return(0,H.jsxs)(`div`,{className:`attr-block`,children:[(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:r,onChange:e=>i(e.target.checked)}),` `,n(`attr.gigagroup`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:a,onChange:e=>o(e.target.checked)}),` `,n(`attr.antispam`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:s,onChange:e=>c(e.target.checked)}),` `,n(`attr.participantsHidden`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:l,onChange:e=>u(e.target.checked)}),` `,n(`attr.noforwards`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:d,onChange:e=>f(e.target.checked)}),` `,n(`attr.joinToSend`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>m(e.target.checked)}),` `,n(`attr.joinRequest`)]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`attr.slowmode`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,max:`86400`,value:h,onChange:e=>_(e.target.value)})]}),(0,H.jsx)(Q,{label:n(`attr.applySettings`),icon:(0,H.jsx)(ze,{size:15}),tone:`warn`,path:`/api/actions/set-channel-settings`,payload:v,onDone:t})]})}function gn({id:e,navigate:t}){let{t:n}=U(),[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)(`1000`),[p,m]=(0,g.useState)(()=>_n(new Date(Date.now()+7*864e5))),[h,_]=(0,g.useState)(``);async function v(){c(!0),o(``);try{let t=await k.account(e);i(t),t.Restriction.Frozen&&(t.Restriction.Until&&m(_n(new Date(t.Restriction.Until))),_(t.Restriction.AppealURL||``))}catch(e){o(O(e))}finally{c(!1)}}if((0,g.useEffect)(()=>{v()},[e]),a)return(0,H.jsx)(J,{children:a});if(!r)return(0,H.jsx)(Nt,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,H.jsx)(K,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,H.jsx)(ae,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(kt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:pt(y)}),(0,H.jsxs)(`div`,{className:`entity-subtitle`,children:[W(y.Username)||n(`account.noUsername`),` · `,ft(y.Phone)||n(`account.noPhone`)]}),y.Collectibles?.length>0&&(0,H.jsx)(`div`,{className:`entity-subtitle`,children:(0,H.jsx)(Ft,{username:``,collectibles:y.Collectibles})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,H.jsx)(Y,{tone:`good`,children:n(`account.premium`)}):(0,H.jsx)(Y,{children:n(`account.notPremium`)}),r.Verified?(0,H.jsx)(Y,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(Y,{children:n(`account.notVerified`)}),(0,H.jsx)(ln,{scam:r.Scam,fake:r.Fake}),y.Frozen?(0,H.jsx)(Y,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,H.jsx)(Y,{children:n(`account.accountActive`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,H.jsx)(Z,{label:n(`account.lastActive`),value:ht(r.LastSeenAt)||`-`}),(0,H.jsx)(Z,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?ht(y.PremiumUntil):n(`common.none`)}),(0,H.jsx)(Z,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,H.jsx)(Z,{label:n(`common.updatedAt`),value:G(y.UpdatedAt)||`-`}),(0,H.jsx)(Z,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,H.jsx)(Z,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,H.jsx)(Z,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,H.jsx)(Z,{label:n(`account.freezeSince`),value:r.Restriction.Since?G(r.Restriction.Since):n(`common.none`)}),(0,H.jsx)(Z,{label:n(`account.freezeUntil`),value:r.Restriction.Until?G(r.Restriction.Until):n(`common.none`)}),(0,H.jsx)(Z,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,H.jsx)(Z,{label:n(`account.createdAt`),value:G(y.CreatedAt)||`-`})]}),r.About&&(0,H.jsx)(`p`,{className:`about-text`,children:r.About}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,H.jsx)(cn,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(jt,{rows:r.AuditLogs})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,H.jsx)(Q,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,H.jsx)(ee,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!0,freeze_until:new Date(p).toISOString(),freeze_appeal_url:h.trim()}),onDone:v}),y.Frozen&&(0,H.jsx)(Q,{label:n(`account.unfreezeAccount`),icon:(0,H.jsx)(ee,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,H.jsxs)(`div`,{className:`action-stack`,children:[(0,H.jsx)(Q,{label:n(`account.setPremium`),icon:(0,H.jsx)(re,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:_t(l)}),onDone:v}),(0,H.jsx)(Q,{label:n(`account.clearPremium`),icon:(0,H.jsx)(re,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,H.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,H.jsx)(Q,{label:n(`account.grantStars`),icon:(0,H.jsx)(Ke,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:_t(d)}),onDone:v}),(0,H.jsx)(Q,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,H.jsx)(F,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:y.ID,verified:!r.Verified}),onDone:v})]}),(0,H.jsx)(un,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-flags`,scam:r.Scam,fake:r.Fake,onDone:v}),(0,H.jsx)(`div`,{className:`dock-title`,children:n(`attr.attributes`)}),(0,H.jsx)(dn,{id:y.ID,support:r.Support,onDone:v}),(0,H.jsx)(fn,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-username`,current:y.Username,onDone:v}),(0,H.jsx)(pn,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-color`,onDone:v}),(0,H.jsx)(mn,{idKey:`user_id`,id:y.ID,path:`/api/actions/set-account-emoji-status`,onDone:v})]})})})}function _n(e){return new Date(e.getTime()-e.getTimezoneOffset()*6e4).toISOString().slice(0,16)}function vn({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(`50`),[c,l]=(0,g.useState)([]),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!1),[_,v]=(0,g.useState)(``);async function y(e=!1){let t=i.trim();h(!0),v(``);let r=new URLSearchParams({limit:o});n.trim()&&r.set(`min_level`,n.trim()),t&&r.set(`q`,t),e&&f&&r.set(`before_id`,f);try{let t=await k.accountRatings(r),n=t.rows??[];l(t=>e?[...t,...n]:n),p(t.next_before_id??``),d(!!t.has_more)}catch(e){v(O(e))}finally{h(!1)}}(0,g.useEffect)(()=>{y(!1)},[]);let b=c.reduce((e,t)=>Math.max(e,t.Level),0),x=c.filter(e=>vt(e.PendingStars)!==0).length,S=c.length>0?(c.reduce((e,t)=>e+t.Level,0)/c.length).toFixed(1):`0`;return(0,H.jsxs)(K,{title:t(`rating.pageTitle`),eyebrow:t(`rating.eyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>y(!1),disabled:m,children:[(0,H.jsx)(Fe,{size:15,className:m?`spin`:``}),` `,t(`common.refresh`)]}),children:[_&&(0,H.jsx)(J,{children:_}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(X,{label:t(`rating.metricLoaded`),value:String(c.length)}),(0,H.jsx)(X,{label:t(`rating.metricTopLevel`),value:String(b),tone:`good`}),(0,H.jsx)(X,{label:t(`rating.metricAvgLevel`),value:S}),(0,H.jsx)(X,{label:t(`rating.metricPending`),value:String(x),tone:x?`warn`:`neutral`})]}),(0,H.jsx)(Ot,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),y(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`rating.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`rating.minLevel`)}),(0,H.jsx)(`input`,{className:`small-input`,value:n,onChange:e=>r(e.target.value),type:`number`,min:`0`,placeholder:`0`})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:o,onChange:e=>s(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:m,children:[m?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(Ie,{size:15}),` `,t(`common.search`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`rating.userID`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`rating.level`)}),(0,H.jsx)(`th`,{children:t(`rating.stars`)}),(0,H.jsx)(`th`,{children:t(`rating.progress`)}),(0,H.jsx)(`th`,{children:t(`rating.pending`)}),(0,H.jsx)(`th`,{children:t(`rating.computedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[c.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.UserID}),(0,H.jsx)(`td`,{children:W(n.Username)||n.FirstName||`-`}),(0,H.jsx)(`td`,{children:(0,H.jsx)(yn,{level:n.Level})}),(0,H.jsx)(`td`,{className:`mono`,children:yt(n.Stars)}),(0,H.jsx)(`td`,{children:(0,H.jsx)(xn,{row:n})}),(0,H.jsx)(`td`,{className:`mono`,children:vt(n.PendingStars)===0?`-`:yt(n.PendingStars)}),(0,H.jsx)(`td`,{children:G(n.ComputedAt)||`-`}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/account-ratings/${n.UserID}`),children:[(0,H.jsx)(Xe,{size:14}),` `,t(`common.detail`),` `,(0,H.jsx)(z,{size:14})]})})]},n.UserID)),c.length===0&&(0,H.jsx)(Mt,{colSpan:8})]})]})}),u&&(0,H.jsx)(`div`,{className:`toolbar`,children:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>y(!0),disabled:m,children:[m?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(fe,{size:15}),` `,t(`common.loadMore`)]})})]})}function yn({level:e}){let{t}=U();return(0,H.jsx)(Y,{tone:e>=10?`good`:e>=5?`warn`:`neutral`,children:t(`rating.levelValue`,{level:e})})}function bn(e){let t=vt(e.Stars),n=vt(e.CurrentLevelStars),r=vt(e.NextLevelStars),i=r-n;return{percent:i>0?Math.min(100,Math.max(0,(t-n)/i*100)):0,remaining:Math.max(0,r-t),target:r,stars:t}}function xn({row:e}){let{t}=U();if(!e.HasNextLevel)return(0,H.jsx)(`span`,{className:`progress-note`,children:t(`rating.maxLevel`)});let{percent:n,remaining:r,target:i}=bn(e);return(0,H.jsxs)(`div`,{className:`progress-cell`,children:[(0,H.jsx)(`div`,{className:`progress-bar`,role:`img`,"aria-label":`${Math.round(n)}%`,children:(0,H.jsx)(`span`,{style:{width:`${n}%`}})}),(0,H.jsx)(`small`,{children:t(`rating.progressHint`,{remaining:yt(String(r)),target:yt(String(i))})})]})}function Sn({userID:e,navigate:t}){let{t:n}=U(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(``);async function d(){c(!0),o(``);try{i(await k.accountRating(e))}catch(e){o(O(e))}finally{c(!1)}}if((0,g.useEffect)(()=>{d()},[e]),a&&!r)return(0,H.jsx)(J,{children:a});if(!r)return(0,H.jsx)(Nt,{label:n(s?`rating.loadingDetail`:`account.waitingData`)});let f=r.rating,p=r.events??[],m=vt(f.PendingStars),h=bn(f),_=f.UserID||e;return(0,H.jsxs)(K,{title:n(`rating.detailTitle`,{user:W(f.Username)||f.FirstName||f.UserID}),eyebrow:n(`rating.detailEyebrow`),actions:(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/account-ratings`),children:[(0,H.jsx)(ae,{size:15}),` `,n(`common.backToList`)]}),(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:d,disabled:s,children:[(0,H.jsx)(Fe,{size:15,className:s?`spin`:``}),` `,n(`common.refresh`)]})]}),children:[a&&(0,H.jsx)(J,{children:a}),(0,H.jsx)(kt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:W(f.Username)||f.FirstName||n(`bots.unnamed`)}),(0,H.jsxs)(`div`,{className:`entity-subtitle`,children:[n(`rating.userID`),`: `,f.UserID]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(yn,{level:f.Level}),m!==0&&(0,H.jsx)(Y,{tone:`warn`,children:n(`rating.pendingBadge`,{amount:Et(f.PendingStars)})})]})]}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(X,{label:n(`rating.stars`),value:yt(f.Stars),mono:!0}),(0,H.jsx)(X,{label:n(`rating.level`),value:String(f.Level),tone:`good`}),(0,H.jsx)(X,{label:n(`rating.nextLevel`),value:f.HasNextLevel?yt(f.NextLevelStars):n(`rating.maxLevel`),mono:f.HasNextLevel}),(0,H.jsx)(X,{label:n(`rating.toNextLevel`),value:f.HasNextLevel?yt(String(h.remaining)):`-`,mono:!0,tone:f.HasNextLevel&&h.percent>=80?`good`:`neutral`})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`rating.breakdownTitle`),text:n(`rating.breakdownHint`)}),(0,H.jsx)(Cn,{rating:f}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`rating.currentLevelStars`),value:yt(f.CurrentLevelStars),mono:!0}),(0,H.jsx)(Z,{label:n(`rating.nextLevelStars`),value:f.HasNextLevel?yt(f.NextLevelStars):n(`rating.maxLevel`),mono:f.HasNextLevel}),(0,H.jsx)(Z,{label:n(`rating.computedAt`),value:G(f.ComputedAt)||`-`}),(0,H.jsx)(Z,{label:n(`common.updatedAt`),value:G(f.UpdatedAt)||`-`})]}),(0,H.jsx)(`div`,{className:`progress-wide`,children:(0,H.jsx)(xn,{row:f})})]}),m!==0&&(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`rating.pendingTitle`),text:n(`rating.pendingHint`)}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`rating.pending`),value:Et(f.PendingStars),mono:!0}),(0,H.jsx)(Z,{label:n(`rating.pendingDate`),value:G(f.PendingDate)||`-`})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`rating.eventsTitle`),text:n(`rating.eventsHint`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:n(`common.id`)}),(0,H.jsx)(`th`,{children:n(`rating.eventKind`)}),(0,H.jsx)(`th`,{children:n(`rating.amount`)}),(0,H.jsx)(`th`,{children:n(`audit.reason`)}),(0,H.jsx)(`th`,{children:n(`audit.actor`)}),(0,H.jsx)(`th`,{children:n(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[p.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:e.ID}),(0,H.jsx)(`td`,{children:(0,H.jsx)(wn,{kind:e.Kind})}),(0,H.jsx)(`td`,{className:`mono`,children:Et(e.Amount)}),(0,H.jsx)(`td`,{className:`truncate`,children:e.Reason||`-`}),(0,H.jsx)(`td`,{children:e.Actor||`-`}),(0,H.jsx)(`td`,{children:G(e.CreatedAt)||`-`})]},e.ID)),p.length===0&&(0,H.jsx)(Mt,{colSpan:6})]})]})})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`rating.actionDock`)}),(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/accounts/${f.UserID}`),children:[(0,H.jsx)($e,{size:15}),` `,n(`rating.openAccount`)]}),(0,H.jsx)(`div`,{className:`action-stack`,children:(0,H.jsx)(Q,{label:n(`rating.recompute`),icon:(0,H.jsx)(de,{size:15}),tone:`neutral`,path:`/api/actions/recompute-account-rating`,payload:()=>({user_id:_}),onDone:d})}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`rating.recomputeHint`)}),(0,H.jsx)(`div`,{className:`dock-title`,children:n(`rating.adjustTitle`)}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`rating.adjustAmount`)}),(0,H.jsx)(`input`,{value:l,onChange:e=>u(e.target.value),type:`number`,step:`1`,placeholder:`-500`})]}),(0,H.jsx)(`div`,{className:`action-stack`,children:(0,H.jsx)(Q,{label:n(`rating.adjust`),icon:(0,H.jsx)(V,{size:15}),tone:`warn`,path:`/api/actions/adjust-account-rating`,payload:()=>({user_id:_,amount:String(Number.parseInt(l.trim()||`0`,10)||0)}),onDone:()=>{u(``),d()}})}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`rating.adjustHint`)})]})})]})}function Cn({rating:e}){let{t}=U(),n=[{key:`stars`,label:t(`rating.componentStars`),hint:t(`rating.componentStarsHint`),value:vt(e.StarsComponent)},{key:`activity`,label:t(`rating.componentActivity`),hint:t(`rating.componentActivityHint`),value:vt(e.ActivityComponent)},{key:`penalty`,label:t(`rating.componentPenalty`),hint:t(`rating.componentPenaltyHint`),value:-vt(e.PenaltyComponent)},{key:`manual`,label:t(`rating.componentManual`),hint:t(`rating.componentManualHint`),value:vt(e.ManualComponent)}],r=Math.max(1,...n.map(e=>Math.abs(e.value))),i=Math.max(0,n.reduce((e,t)=>e+t.value,0)),a=vt(e.Stars),o=vt(e.PendingStars);return(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`div`,{className:`breakdown-list`,children:[n.map(e=>{let t=Math.min(100,Math.abs(e.value)/r*100),n=e.value<0?`danger`:e.value>0?`good`:``;return(0,H.jsxs)(`div`,{className:`breakdown-row`,children:[(0,H.jsxs)(`div`,{className:`breakdown-label`,children:[(0,H.jsx)(`strong`,{children:e.label}),(0,H.jsx)(`small`,{children:e.hint})]}),(0,H.jsx)(`div`,{className:`progress-bar ${n}`,role:`img`,"aria-label":String(e.value),children:(0,H.jsx)(`span`,{style:{width:`${t}%`}})}),(0,H.jsx)(`div`,{className:`breakdown-value mono ${n}`,children:Et(String(e.value))})]},e.key)}),(0,H.jsxs)(`div`,{className:`breakdown-row total`,children:[(0,H.jsx)(`div`,{className:`breakdown-label`,children:(0,H.jsx)(`strong`,{children:t(`rating.componentTotal`)})}),(0,H.jsx)(`div`,{className:`breakdown-value mono`,children:yt(e.Stars)})]})]}),o===0&&i!==a&&(0,H.jsx)(J,{children:t(`rating.breakdownMismatch`,{sum:yt(String(i)),total:yt(e.Stars)})}),o!==0&&(0,H.jsx)(`p`,{className:`bot-create-note`,children:t(`rating.breakdownPending`,{amount:Et(e.PendingStars)})})]})}function wn({kind:e}){let{t}=U();return(0,H.jsx)(Y,{tone:e===`moderation`?`danger`:e===`manual`?`warn`:e===`recompute`?`neutral`:`good`,children:t(`rating.kind.${e}`)})}function Tn(e){return e.reduce((e,t)=>(e.devices+=t.DeviceCount,t.PremiumUntil>0&&(e.premium+=1),t.Frozen&&(e.frozen+=1),e),{devices:0,premium:0,frozen:0})}function En(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})}function Dn({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`50`),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)({beforeID:0,beforeActiveUS:0}),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``);async function m(e=!1){d(!0),p(``);let t=new URLSearchParams({limit:i});n.trim()?t.set(`q`,n.trim()):e&&(t.set(`before_id`,String(c.beforeID)),t.set(`before_active_us`,String(c.beforeActiveUS)));try{let e=await k.accounts(t);s(e),l({beforeID:e.next_before_id,beforeActiveUS:e.next_before_active_us})}catch(e){p(O(e))}finally{d(!1)}}(0,g.useEffect)(()=>{m(!1)},[]);let h=Tn(o?.rows??[]);return(0,H.jsxs)(K,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,H.jsx)(Fe,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(J,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(X,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,H.jsx)(X,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,H.jsx)(X,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,H.jsx)(X,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,H.jsx)(Ot,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(Ie,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,H.jsx)(z,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`account.userID`)}),(0,H.jsx)(`th`,{children:t(`account.phone`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`common.name`)}),(0,H.jsx)(`th`,{children:t(`common.device`)}),(0,H.jsx)(`th`,{children:t(`account.lastActive`)}),(0,H.jsx)(`th`,{children:t(`account.premium`)}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`account.frozen`)}),(0,H.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:ft(n.Phone)}),(0,H.jsx)(`td`,{children:(0,H.jsx)(Ft,{username:n.Username,collectibles:n.Collectibles})}),(0,H.jsx)(`td`,{children:pt(n)}),(0,H.jsx)(`td`,{children:n.DeviceCount}),(0,H.jsx)(`td`,{children:G(n.LastActiveAt)}),(0,H.jsx)(`td`,{children:n.PremiumUntil>0?(0,H.jsxs)(Y,{tone:`good`,children:[t(`account.premium`),` `,ht(n.PremiumUntil)]}):(0,H.jsx)(Y,{children:t(`common.none`)})}),(0,H.jsxs)(`td`,{children:[n.Verified?(0,H.jsx)(Y,{tone:`good`,children:t(`common.verified`)}):(0,H.jsx)(Y,{children:t(`account.notVerified`)}),` `,(0,H.jsx)(ln,{scam:n.Scam,fake:n.Fake})]}),(0,H.jsx)(`td`,{children:n.Frozen?(0,H.jsx)(Y,{tone:`danger`,children:t(`account.frozen`)}):(0,H.jsx)(Y,{children:t(`common.normal`)})}),(0,H.jsx)(`td`,{children:G(n.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(z,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,H.jsx)(Mt,{colSpan:11})]})]})})]})}function On({label:e,value:t,onChange:n}){let{t:r}=U(),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);let e=new URLSearchParams({limit:`20`});i.trim()&&e.set(`q`,i.trim());try{s((await k.accounts(e)).rows)}catch(e){d(O(e))}finally{l(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,H.jsxs)(`div`,{className:`entity-picker`,children:[(0,H.jsxs)(`div`,{className:`picker-head`,children:[(0,H.jsx)(`span`,{children:e}),t?(0,H.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,H.jsx)(nt,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,H.jsxs)(`div`,{className:`selected-entity`,children:[(0,H.jsx)(R,{size:15}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:pt(t)}),(0,H.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,H.jsx)(`span`,{children:W(t.Username)||ft(t.Phone)||`-`})]}):null,(0,H.jsxs)(`div`,{className:`picker-search`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,H.jsx)(L,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,H.jsx)(`div`,{className:`picker-error`,children:u}),(0,H.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,H.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.ID}),(0,H.jsx)(`strong`,{children:pt(e)}),(0,H.jsx)(`span`,{children:W(e.Username)||ft(e.Phone)||`-`}),e.Verified?(0,H.jsx)(Y,{tone:`good`,children:r(`picker.verified`)}):(0,H.jsx)(Y,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,H.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function kn({label:e,value:t,onChange:n}){let{t:r}=U(),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);let e=new URLSearchParams({limit:`20`});i.trim()&&e.set(`q`,i.trim().replace(/^@/,``));try{s((await k.bots(e)).rows??[])}catch(e){d(O(e))}finally{l(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,H.jsxs)(`div`,{className:`entity-picker`,children:[(0,H.jsxs)(`div`,{className:`picker-head`,children:[(0,H.jsx)(`span`,{children:e}),t?(0,H.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,H.jsx)(nt,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,H.jsxs)(`div`,{className:`selected-entity`,children:[(0,H.jsx)(R,{size:15}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:t.FirstName||`-`}),(0,H.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,H.jsx)(`span`,{children:W(t.Username)||`-`})]}):null,(0,H.jsxs)(`div`,{className:`picker-search`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.botPlaceholder`)}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,H.jsx)(L,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,H.jsx)(`div`,{className:`picker-error`,children:u}),(0,H.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,H.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.ID}),(0,H.jsx)(`strong`,{children:e.FirstName||`-`}),(0,H.jsx)(`span`,{children:W(e.Username)||`-`}),e.System?(0,H.jsx)(Y,{tone:`warn`,children:r(`picker.system`)}):(0,H.jsx)(Y,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,H.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function An({label:e,value:t,onChange:n}){let{t:r}=U(),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);let e=new URLSearchParams({limit:`20`});i.trim()&&e.set(`q`,i.trim());try{s((await k.channels(e)).rows)}catch(e){d(O(e))}finally{l(!1)}}return(0,g.useEffect)(()=>{f()},[]),(0,H.jsxs)(`div`,{className:`entity-picker`,children:[(0,H.jsxs)(`div`,{className:`picker-head`,children:[(0,H.jsx)(`span`,{children:e}),t?(0,H.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,H.jsx)(nt,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,H.jsxs)(`div`,{className:`selected-entity`,children:[(0,H.jsx)(R,{size:15}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:t.Title||`-`}),(0,H.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,H.jsx)(`span`,{children:W(t.Username)||mt(t,r)})]}):null,(0,H.jsxs)(`div`,{className:`picker-search`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,H.jsx)(L,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,H.jsx)(`div`,{className:`picker-error`,children:u}),(0,H.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,H.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.ID}),(0,H.jsx)(`strong`,{children:e.Title||`-`}),(0,H.jsx)(`span`,{children:W(e.Username)||mt(e,r)}),e.Verified?(0,H.jsx)(Y,{tone:`good`,children:r(`picker.verified`)}):(0,H.jsx)(Y,{children:mt(e,r)})]},e.ID)),o.length===0&&!c?(0,H.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function jn({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(`all`),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(`50`),[c,l]=(0,g.useState)([]),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!1),[_,v]=(0,g.useState)(``),[y,b]=(0,g.useState)(`vault`),[x,S]=(0,g.useState)(null),[C,w]=(0,g.useState)(null),[T,E]=(0,g.useState)(``),[D,A]=(0,g.useState)(`XTR`),[j,M]=(0,g.useState)(``),[N,P]=(0,g.useState)(``),[F,ee]=(0,g.useState)(``),[I,te]=(0,g.useState)(``),[ne,re]=(0,g.useState)(``),[ie,ae]=(0,g.useState)(``);async function se(e=!1){h(!0),v(``);let t=new URLSearchParams({limit:o});n!==`all`&&t.set(`status`,n),i.trim()&&t.set(`q`,i.trim().replace(/^@/,``)),e&&f&&t.set(`before_id`,f);try{let n=await k.collectibleUsernames(t),r=n.rows??[];l(t=>e?[...t,...r]:r),p(n.next_before_id??``),d(!!n.has_more)}catch(e){v(O(e))}finally{h(!1)}}(0,g.useEffect)(()=>{se(!1)},[]);let ce=c.filter(e=>e.Status===`vault`).length,le=c.filter(e=>e.Status===`owned`).length,ue=c.filter(e=>e.Status===`burned`).length,de=Tt(j,D),R=N?Tt(F,N):`0`,pe=de===null,me=R===null;function he(){let e={username:T.trim().replace(/^@/,``),currency:D,amount:de??`0`};if(y===`user`&&x&&(e.owner_user_id=String(x.ID)),y===`channel`&&C&&(e.owner_channel_id=String(C.ID)),N&&(e.crypto_currency=N,e.crypto_amount=R??`0`),I.trim()&&(e.url=I.trim()),ne){let t=Date.parse(`${ne}T${ie||`00:00`}:00Z`);Number.isFinite(t)&&(e.purchase_date=Math.floor(t/1e3))}return e}return(0,H.jsxs)(K,{title:t(`usernames.pageTitle`),eyebrow:t(`usernames.eyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>se(!1),disabled:m,children:[(0,H.jsx)(Fe,{size:15,className:m?`spin`:``}),` `,t(`common.refresh`)]}),children:[_&&(0,H.jsx)(J,{children:_}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(X,{label:t(`usernames.metricLoaded`),value:String(c.length)}),(0,H.jsx)(X,{label:t(`usernames.metricVault`),value:String(ce)}),(0,H.jsx)(X,{label:t(`usernames.metricOwned`),value:String(le),tone:`good`}),(0,H.jsx)(X,{label:t(`usernames.metricBurned`),value:String(ue),tone:ue?`danger`:`neutral`})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:t(`usernames.mintTitle`),text:t(`usernames.mintHint`)}),(0,H.jsxs)(`div`,{className:`toolbar`,role:`group`,"aria-label":t(`usernames.ownerKind`),children:[(0,H.jsxs)(`button`,{type:`button`,className:`btn ${y===`vault`?`primary`:``}`,onClick:()=>b(`vault`),children:[(0,H.jsx)(tt,{size:15}),` `,t(`usernames.ownerVault`)]}),(0,H.jsx)(`button`,{type:`button`,className:`btn ${y===`user`?`primary`:``}`,onClick:()=>b(`user`),children:t(`usernames.ownerUser`)}),(0,H.jsx)(`button`,{type:`button`,className:`btn ${y===`channel`?`primary`:``}`,onClick:()=>b(`channel`),children:t(`usernames.ownerChannel`)})]}),y===`user`&&(0,H.jsx)(On,{label:t(`usernames.ownerUser`),value:x,onChange:S}),y===`channel`&&(0,H.jsx)(An,{label:t(`usernames.ownerChannel`),value:C,onChange:w}),(0,H.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`common.username`)}),(0,H.jsx)(`input`,{value:T,onChange:e=>E(e.target.value),placeholder:`durov`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`usernames.currency`)}),(0,H.jsxs)(`select`,{value:D,onChange:e=>A(e.target.value),children:[(0,H.jsx)(`option`,{value:`XTR`,children:`XTR`}),(0,H.jsx)(`option`,{value:`TON`,children:`TON`}),(0,H.jsx)(`option`,{value:`USD`,children:`USD`})]})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`usernames.amount`,{currency:D})}),(0,H.jsx)(`input`,{value:j,onChange:e=>M(e.target.value),inputMode:`decimal`,placeholder:`1000`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`usernames.cryptoCurrency`)}),(0,H.jsxs)(`select`,{value:N,onChange:e=>P(e.target.value),children:[(0,H.jsx)(`option`,{value:``,children:t(`usernames.cryptoNone`)}),(0,H.jsx)(`option`,{value:`TON`,children:`TON`})]})]}),N!==``&&(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`usernames.cryptoAmount`,{currency:N})}),(0,H.jsx)(`input`,{value:F,onChange:e=>ee(e.target.value),inputMode:`decimal`,placeholder:`12.5`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`usernames.url`)}),(0,H.jsx)(`input`,{value:I,onChange:e=>te(e.target.value),placeholder:`https://fragment.com/username/durov`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`usernames.purchaseDate`)}),(0,H.jsx)(`input`,{value:ne,onChange:e=>re(e.target.value),type:`date`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`usernames.purchaseTime`)}),(0,H.jsx)(`input`,{value:ie,onChange:e=>ae(e.target.value),type:`time`,step:60,disabled:!ne})]})]}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:t(`usernames.amountHint`,{currency:D,decimals:String(xt(D)),preview:wt(de??`0`,D)})}),pe&&(0,H.jsx)(J,{children:t(`usernames.amountInvalid`,{currency:D,decimals:String(xt(D))})}),N!==``&&me&&(0,H.jsx)(J,{children:t(`usernames.amountInvalid`,{currency:N,decimals:String(xt(N))})}),(0,H.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,H.jsx)(`span`,{className:`bot-create-note`,children:t(`usernames.mintNote`)}),(0,H.jsx)(Q,{disabled:pe||me,label:t(`usernames.mint`),icon:(0,H.jsx)(Me,{size:15}),tone:`neutral`,path:`/api/actions/mint-collectible-username`,payload:he,onDone:()=>se(!1)})]})]}),(0,H.jsx)(Ot,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),se(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`usernames.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.status`)}),(0,H.jsxs)(`select`,{value:n,onChange:e=>r(e.target.value),children:[(0,H.jsx)(`option`,{value:`all`,children:t(`usernames.statusAll`)}),(0,H.jsx)(`option`,{value:`vault`,children:t(`usernames.statusVault`)}),(0,H.jsx)(`option`,{value:`owned`,children:t(`usernames.statusOwned`)}),(0,H.jsx)(`option`,{value:`burned`,children:t(`usernames.statusBurned`)})]})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:o,onChange:e=>s(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:m,children:[m?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(Ie,{size:15}),` `,t(`common.search`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`common.owner`)}),(0,H.jsx)(`th`,{children:t(`usernames.price`)}),(0,H.jsx)(`th`,{children:t(`usernames.purchaseDate`)}),(0,H.jsx)(`th`,{children:t(`usernames.transfers`)}),(0,H.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[c.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:(0,H.jsx)(`strong`,{children:W(n.Username)})}),(0,H.jsx)(`td`,{children:(0,H.jsx)(Mn,{status:n.Status})}),(0,H.jsx)(`td`,{children:Nn(n,t(`usernames.statusVault`))}),(0,H.jsx)(`td`,{className:`mono`,children:Pn(n)}),(0,H.jsx)(`td`,{children:G(n.PurchaseDate)||`-`}),(0,H.jsx)(`td`,{className:`mono`,children:n.TransferCount}),(0,H.jsx)(`td`,{children:G(n.UpdatedAt)||`-`}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/collectible-usernames/${n.ID}`),children:[(0,H.jsx)(oe,{size:14}),` `,t(`common.detail`),` `,(0,H.jsx)(z,{size:14})]})})]},n.ID)),c.length===0&&(0,H.jsx)(Mt,{colSpan:8})]})]})}),u&&(0,H.jsx)(`div`,{className:`toolbar`,children:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>se(!0),disabled:m,children:[m?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(fe,{size:15}),` `,t(`common.loadMore`)]})})]})}function Mn({status:e}){let{t}=U();return e===`owned`?(0,H.jsx)(Y,{tone:`good`,children:t(`usernames.statusOwned`)}):e===`burned`?(0,H.jsxs)(Y,{tone:`danger`,children:[(0,H.jsx)(ye,{size:12}),` `,t(`usernames.statusBurned`)]}):(0,H.jsxs)(Y,{children:[(0,H.jsx)(tt,{size:12}),` `,t(`usernames.statusVault`)]})}function Nn(e,t){return!e.OwnerPeerType||e.OwnerPeerID===``||e.OwnerPeerID===`0`?t:`${W(e.OwnerUsername)||e.OwnerName||e.OwnerPeerID} · ${e.OwnerPeerType}:${e.OwnerPeerID}`}function Pn(e){let t=wt(e.Amount,e.Currency);return e.CryptoCurrency&&e.CryptoAmount&&e.CryptoAmount!==`0`?`${t} (${wt(e.CryptoAmount,e.CryptoCurrency)})`:t}function Fn({id:e,navigate:t}){let{t:n}=U(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(!1),[l,u]=(0,g.useState)(`user`),[d,f]=(0,g.useState)(null),[p,m]=(0,g.useState)(null);async function h(){c(!0),o(``);try{i(await k.collectibleUsername(e))}catch(e){o(O(e))}finally{c(!1)}}if((0,g.useEffect)(()=>{h()},[e]),a&&!r)return(0,H.jsx)(J,{children:a});if(!r)return(0,H.jsx)(Nt,{label:n(s?`usernames.loadingDetail`:`account.waitingData`)});let _=r.asset,v=r.transfers??[],y=n(`usernames.statusVault`),b=!!_.OwnerPeerType&&_.OwnerPeerID!==``&&_.OwnerPeerID!==`0`,x=_.Status===`burned`;function S(){b&&t(_.OwnerPeerType===`channel`?`/channels/${_.OwnerPeerID}`:`/accounts/${_.OwnerPeerID}`)}function C(){let e={username:_.Username};return l===`user`&&d&&(e.to_user_id=String(d.ID)),l===`channel`&&p&&(e.to_channel_id=String(p.ID)),e}return(0,H.jsxs)(K,{title:n(`usernames.detailTitle`,{username:W(_.Username)}),eyebrow:n(`usernames.detailEyebrow`),actions:(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/collectible-usernames`),children:[(0,H.jsx)(ae,{size:15}),` `,n(`common.backToList`)]}),(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:h,disabled:s,children:[(0,H.jsx)(Fe,{size:15,className:s?`spin`:``}),` `,n(`common.refresh`)]})]}),children:[a&&(0,H.jsx)(J,{children:a}),(0,H.jsx)(kt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:W(_.Username)}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:n(`usernames.assetID`,{id:_.ID})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(Mn,{status:_.Status}),(0,H.jsx)(Y,{tone:_.TransferCount>0?`warn`:`neutral`,children:n(`usernames.transferCount`,{count:_.TransferCount})}),_.Status===`owned`&&(0,H.jsx)(Y,{tone:_.RegistryActive?`good`:`warn`,children:_.RegistryActive?n(`usernames.registryActive`):n(`usernames.registryHidden`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`common.owner`),value:Nn(_,y)}),(0,H.jsx)(Z,{label:n(`usernames.price`),value:Pn(_),mono:!0}),(0,H.jsx)(Z,{label:n(`usernames.purchaseDate`),value:G(_.PurchaseDate)||`-`}),(0,H.jsx)(Z,{label:n(`usernames.originalOwner`),value:Ln(_.OriginalOwnerPeerType,_.OriginalOwnerPeerID,y,_.OriginalOwnerUsername)}),(0,H.jsx)(Z,{label:n(`usernames.transfers`),value:String(_.TransferCount),mono:!0}),(0,H.jsx)(Z,{label:n(`account.createdAt`),value:G(_.CreatedAt)||`-`}),(0,H.jsx)(Z,{label:n(`common.updatedAt`),value:G(_.UpdatedAt)||`-`})]}),(0,H.jsxs)(`div`,{className:`toolbar`,children:[b&&(0,H.jsx)(`button`,{className:`row-link`,type:`button`,onClick:S,children:_.OwnerPeerType===`channel`?n(`usernames.openOwnerChannel`):n(`usernames.openOwnerAccount`)}),_.URL&&(0,H.jsxs)(`a`,{className:`row-link`,href:_.URL,target:`_blank`,rel:`noreferrer noopener`,children:[(0,H.jsx)(ge,{size:14}),` `,n(`usernames.openMarketplace`)]})]}),!x&&(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`usernames.transferTitle`),text:n(`usernames.transferHint`)}),(0,H.jsxs)(`div`,{className:`toolbar`,role:`group`,"aria-label":n(`usernames.recipientKind`),children:[(0,H.jsx)(`button`,{type:`button`,className:`btn ${l===`user`?`primary`:``}`,onClick:()=>u(`user`),children:n(`usernames.recipientUser`)}),(0,H.jsx)(`button`,{type:`button`,className:`btn ${l===`channel`?`primary`:``}`,onClick:()=>u(`channel`),children:n(`usernames.recipientChannel`)})]}),l===`user`?(0,H.jsx)(On,{label:n(`usernames.recipientUser`),value:d,onChange:f}):(0,H.jsx)(An,{label:n(`usernames.recipientChannel`),value:p,onChange:m}),(0,H.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,H.jsx)(`span`,{className:`bot-create-note`,children:n(`usernames.transferNote`)}),(0,H.jsx)(Q,{label:n(`usernames.transfer`),icon:(0,H.jsx)(ie,{size:15}),tone:`warn`,path:`/api/actions/transfer-collectible-username`,payload:C,onDone:h})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`usernames.historyTitle`),text:n(`usernames.historyHint`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:n(`common.id`)}),(0,H.jsx)(`th`,{children:n(`usernames.eventKind`)}),(0,H.jsx)(`th`,{children:n(`usernames.fromPeer`)}),(0,H.jsx)(`th`,{children:n(`usernames.toPeer`)}),(0,H.jsx)(`th`,{children:n(`usernames.price`)}),(0,H.jsx)(`th`,{children:n(`audit.actor`)}),(0,H.jsx)(`th`,{children:n(`audit.reason`)}),(0,H.jsx)(`th`,{children:n(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[v.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:e.ID}),(0,H.jsx)(`td`,{children:(0,H.jsx)(In,{kind:e.Kind})}),(0,H.jsx)(`td`,{className:`mono`,children:Ln(e.FromPeerType,e.FromPeerID,y,e.FromUsername)}),(0,H.jsx)(`td`,{className:`mono`,children:Ln(e.ToPeerType,e.ToPeerID,y,e.ToUsername)}),(0,H.jsx)(`td`,{className:`mono`,children:e.Amount&&e.Amount!==`0`?wt(e.Amount,e.Currency):`-`}),(0,H.jsx)(`td`,{children:e.Actor||`-`}),(0,H.jsx)(`td`,{className:`truncate`,children:e.Reason||`-`}),(0,H.jsx)(`td`,{children:G(e.CreatedAt)||`-`})]},e.ID)),v.length===0&&(0,H.jsx)(Mt,{colSpan:8})]})]})})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`usernames.actionDock`)}),x?(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`usernames.burnedHint`)}):(0,H.jsxs)(H.Fragment,{children:[(0,H.jsx)(`div`,{className:`action-stack`,children:(0,H.jsx)(Q,{label:n(`usernames.revoke`),icon:(0,H.jsx)(Ze,{size:15}),tone:`warn`,path:`/api/actions/revoke-collectible-username`,payload:()=>({username:_.Username,burn:!1}),onDone:h})}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`usernames.revokeHint`)}),(0,H.jsxs)(`div`,{className:`danger-zone`,children:[(0,H.jsx)(Q,{label:n(`usernames.burn`),icon:(0,H.jsx)(ye,{size:15}),tone:`danger`,path:`/api/actions/revoke-collectible-username`,payload:()=>({username:_.Username,burn:!0}),onDone:h}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`usernames.burnHint`)}),(0,H.jsx)(Q,{label:n(`usernames.delete`),icon:(0,H.jsx)(Ye,{size:15}),tone:`danger`,path:`/api/actions/delete-collectible-username`,payload:()=>({username:_.Username}),onDone:()=>t(`/collectible-usernames`)}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`usernames.deleteHint`)})]})]})]})})]})}function In({kind:e}){let{t}=U();return(0,H.jsx)(Y,{tone:e===`burn`?`danger`:e===`revoke`?`warn`:e===`mint`?`good`:`neutral`,children:t(`usernames.kind.${e}`)})}function Ln(e,t,n,r=``){if(!e||t===``||t===`0`)return n;let i=W(r);return i?`${i} · ${e}:${t}`:`${e}:${t}`}function Rn({id:e,navigate:t}){let{t:n}=U(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``);async function s(){o(``);try{i(await k.channel(e))}catch(e){o(O(e))}}if((0,g.useEffect)(()=>{s()},[e]),a)return(0,H.jsx)(J,{children:a});if(!r)return(0,H.jsx)(Nt,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,H.jsx)(K,{title:`${mt(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,H.jsx)(ae,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(kt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,H.jsxs)(`div`,{className:`entity-subtitle`,children:[W(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(Y,{children:mt(c,n)}),c.Verified?(0,H.jsx)(Y,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(Y,{children:n(`account.notVerified`)}),(0,H.jsx)(ln,{scam:c.Scam,fake:c.Fake}),c.Deleted?(0,H.jsx)(Y,{tone:`danger`,children:n(`common.deleted`)}):(0,H.jsx)(Y,{children:n(`common.valid`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,H.jsx)(Z,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,H.jsx)(Z,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,H.jsx)(Z,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,H.jsx)(Z,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,H.jsx)(Z,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,H.jsx)(Z,{label:n(`account.createdAt`),value:ht(c.Date)||`-`}),(0,H.jsx)(Z,{label:n(`common.updatedAt`),value:G(c.UpdatedAt)||`-`})]}),c.About&&(0,H.jsx)(`p`,{className:`about-text`,children:c.About}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(jt,{rows:r.AuditLogs})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,H.jsx)(Pt,{value:r.ChannelJSON})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,H.jsx)(Q,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,H.jsx)(F,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:c.ID,verified:!c.Verified}),onDone:s}),(0,H.jsx)(un,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-flags`,scam:c.Scam,fake:c.Fake,onDone:s}),(0,H.jsx)(`div`,{className:`dock-title`,children:n(`attr.settings`)}),(0,H.jsx)(hn,{channel:c,onDone:s}),(0,H.jsx)(`div`,{className:`dock-title`,children:n(`attr.attributes`)}),(0,H.jsx)(fn,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-username`,current:c.Username,onDone:s}),(0,H.jsx)(pn,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-color`,onDone:s}),(0,H.jsx)(mn,{idKey:`channel_id`,id:c.ID,path:`/api/actions/set-channel-emoji-status`,onDone:s})]})})})}function zn({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`50`),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)({beforeID:0,beforeUpdatedUS:0}),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``);async function m(e=!1){d(!0),p(``);let t=new URLSearchParams({limit:i});n.trim()?t.set(`q`,n.trim()):e&&(t.set(`before_id`,String(c.beforeID)),t.set(`before_updated_us`,String(c.beforeUpdatedUS)));try{let e=await k.channels(t);s(e),l({beforeID:e.next_before_id,beforeUpdatedUS:e.next_before_updated_us})}catch(e){p(O(e))}finally{d(!1)}}(0,g.useEffect)(()=>{m(!1)},[]);let h=En(o?.rows??[]);return(0,H.jsxs)(K,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,H.jsx)(Fe,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(J,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(X,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,H.jsx)(X,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,H.jsx)(X,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,H.jsx)(X,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,H.jsx)(Ot,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(Ie,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,H.jsx)(z,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`channel.channelID`)}),(0,H.jsx)(`th`,{children:t(`channel.kind`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`channel.title`)}),(0,H.jsx)(`th`,{children:t(`common.members`)}),(0,H.jsx)(`th`,{children:t(`common.admins`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:mt(n,t)}),(0,H.jsx)(`td`,{children:W(n.Username)}),(0,H.jsx)(`td`,{children:n.Title}),(0,H.jsx)(`td`,{children:n.ParticipantsCount}),(0,H.jsx)(`td`,{children:n.AdminsCount}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsxs)(`td`,{children:[n.Verified?(0,H.jsx)(Y,{tone:`good`,children:t(`common.verified`)}):(0,H.jsx)(Y,{children:t(`account.notVerified`)}),` `,(0,H.jsx)(ln,{scam:n.Scam,fake:n.Fake})]}),(0,H.jsx)(`td`,{children:G(n.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(z,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,H.jsx)(Mt,{colSpan:10})]})]})})]})}function Bn({id:e,navigate:t}){let{t:n}=U(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(!1);async function l(){c(!0),o(``);try{i(await k.bot(e))}catch(e){o(O(e))}finally{c(!1)}}if((0,g.useEffect)(()=>{l()},[e]),a)return(0,H.jsx)(J,{children:a});if(!r)return(0,H.jsx)(Nt,{label:n(s?`bots.loadingDetail`:`account.waitingData`)});let u=r.Bot;return(0,H.jsx)(K,{title:n(`bots.detailTitle`,{id:u.ID}),eyebrow:n(`bots.profile`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/bots`),children:[(0,H.jsx)(ae,{size:15}),` `,n(`common.backToList`)]}),children:(0,H.jsx)(kt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:u.FirstName||n(`bots.unnamed`)}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:W(u.Username)||n(`account.noUsername`)})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(Y,{tone:u.System?`warn`:`neutral`,children:u.System?n(`bots.system`):n(`bots.user`)}),u.Verified?(0,H.jsx)(Y,{tone:`good`,children:n(`common.verified`)}):(0,H.jsx)(Y,{children:n(`account.notVerified`)}),(0,H.jsx)(ln,{scam:u.Scam,fake:u.Fake})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`bots.botID`),value:String(u.ID),mono:!0}),(0,H.jsx)(Z,{label:n(`bots.owner`),value:u.OwnerUserID>0?`${u.OwnerUserID} ${W(r.OwnerUsername)}`.trim():n(`common.none`)}),(0,H.jsx)(Z,{label:n(`bots.type`),value:u.System?n(`bots.system`):n(`bots.user`)}),(0,H.jsx)(Z,{label:n(`common.updatedAt`),value:G(u.UpdatedAt)||`-`}),(0,H.jsx)(Z,{label:n(`account.createdAt`),value:G(u.CreatedAt)||`-`})]}),r.About&&(0,H.jsx)(`p`,{className:`about-text`,children:r.About}),r.Description&&r.Description.trim()!==r.About.trim()&&(0,H.jsx)(`p`,{className:`about-text`,children:r.Description}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,H.jsx)(jt,{rows:r.AuditLogs})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`bots.actionDock`)}),(0,H.jsx)(`div`,{className:`action-stack`,children:(0,H.jsx)(Q,{label:u.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,H.jsx)(F,{size:15}),tone:`neutral`,path:`/api/actions/set-verified`,payload:()=>({user_id:u.ID,verified:!u.Verified}),onDone:l})}),(0,H.jsx)(un,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-flags`,scam:u.Scam,fake:u.Fake,onDone:l}),(0,H.jsx)(`div`,{className:`dock-title`,children:n(`attr.attributes`)}),(0,H.jsx)(fn,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-username`,current:u.Username,onDone:l}),(0,H.jsx)(pn,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-color`,onDone:l}),(0,H.jsx)(mn,{idKey:`user_id`,id:u.ID,path:`/api/actions/set-account-emoji-status`,onDone:l}),u.System?(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`bots.systemHint`)}):(0,H.jsxs)(`div`,{className:`danger-zone`,children:[(0,H.jsx)(Q,{label:n(`bots.delete`),icon:(0,H.jsx)(Ye,{size:15}),tone:`danger`,path:`/api/actions/delete-bot`,payload:()=>({bot_user_id:u.ID}),onDone:()=>t(`/bots`)}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`bots.deleteHint`)})]})]})})})}function Vn({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(``),[i,a]=(0,g.useState)(`50`),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(0),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(``),[y,b]=(0,g.useState)(``);async function x(e=!1){d(!0),p(``);let t=new URLSearchParams({limit:i});n.trim()?t.set(`q`,n.trim()):e&&t.set(`before_id`,String(c));try{let e=await k.bots(t);s(e),l(e.next_before_id)}catch(e){p(O(e))}finally{d(!1)}}(0,g.useEffect)(()=>{x(!1)},[]);let S=o?.rows??[],C=S.filter(e=>e.Verified).length,w=S.filter(e=>e.System).length;return(0,H.jsxs)(K,{title:t(`bots.pageTitle`),eyebrow:o?.listing===!1?t(`bots.queryResults`):t(`bots.recent`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>x(!1),disabled:u,children:[(0,H.jsx)(Fe,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,H.jsx)(J,{children:f}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(X,{label:t(`bots.currentPage`),value:String(S.length)}),(0,H.jsx)(X,{label:t(`common.verified`),value:String(C),tone:`good`}),(0,H.jsx)(X,{label:t(`bots.system`),value:String(w)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(`div`,{className:`section-head`,children:(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`h2`,{children:t(`bots.createTitle`)}),(0,H.jsx)(`p`,{children:t(`bots.createHint`)})]})}),(0,H.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`bots.ownerUserID`)}),(0,H.jsx)(`input`,{value:m,onChange:e=>h(e.target.value),type:`number`,min:`1`,placeholder:`123456789`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`bots.name`)}),(0,H.jsx)(`input`,{value:_,onChange:e=>v(e.target.value),placeholder:t(`bots.namePlaceholder`),maxLength:64})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:t(`bots.username`)}),(0,H.jsx)(`input`,{value:y,onChange:e=>b(e.target.value),placeholder:`my_service_bot`})]})]}),(0,H.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,H.jsx)(`span`,{className:`bot-create-note`,children:t(`bots.usernameHint`)}),(0,H.jsx)(Q,{label:t(`bots.create`),icon:(0,H.jsx)(Me,{size:15}),tone:`neutral`,path:`/api/actions/create-bot`,payload:()=>({owner_user_id:_t(m),name:_.trim(),username:y.trim().replace(/^@/,``)}),onDone:()=>x(!1)})]})]}),(0,H.jsx)(Ot,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),x(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`bots.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(Ie,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>x(!0),disabled:u,children:[(0,H.jsx)(z,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`bots.botID`)}),(0,H.jsx)(`th`,{children:t(`common.username`)}),(0,H.jsx)(`th`,{children:t(`common.name`)}),(0,H.jsx)(`th`,{children:t(`bots.owner`)}),(0,H.jsx)(`th`,{children:t(`common.verified`)}),(0,H.jsx)(`th`,{children:t(`bots.type`)}),(0,H.jsx)(`th`,{children:t(`account.createdAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[S.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:W(n.Username)||`-`}),(0,H.jsx)(`td`,{children:n.FirstName||`-`}),(0,H.jsx)(`td`,{className:`mono`,children:n.OwnerUserID>0?n.OwnerUserID:`-`}),(0,H.jsxs)(`td`,{children:[n.Verified?(0,H.jsxs)(Y,{tone:`good`,children:[(0,H.jsx)(F,{size:12}),` `,t(`common.verified`)]}):(0,H.jsx)(Y,{children:t(`account.notVerified`)}),` `,(0,H.jsx)(ln,{scam:n.Scam,fake:n.Fake})]}),(0,H.jsx)(`td`,{children:n.System?(0,H.jsx)(Y,{tone:`warn`,children:t(`bots.system`)}):(0,H.jsx)(Y,{children:t(`bots.user`)})}),(0,H.jsx)(`td`,{children:G(n.CreatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/bots/${n.ID}`),children:[(0,H.jsx)(ce,{size:14}),` `,t(`common.detail`),` `,(0,H.jsx)(z,{size:14})]})})]},n.ID)),S.length===0&&(0,H.jsx)(Mt,{colSpan:8})]})]})})]})}var Hn=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 z=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 z.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`)},B.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)}},B.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`)))},B.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())},B.prototype.togglePause=function(e){e&&this.name!==e||(this.isPaused===!0?this.play():this.pause())},B.prototype.stop=function(e){e&&this.name!==e||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},B.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`))}},B.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`)},B.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)},B.prototype.playSegments=function(e,t){if(t&&(this.segments.length=0),xe(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}(),we=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),Te=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=we.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),Ee=function(){function e(){return{addedLength:0,percents:p(`float32`,ue()),lengths:p(`float32`,ue())}}return Te(8,e)}(),De=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=Ee.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=Fe(c.s),M=Fe(b),N=(e-y)/(v-y);Pe(r,Ne(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 Pe(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 Fe(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 Ie(){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!==Ae&&(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 Le(e){var t;if(this.propType===`unidimensional`)t=e*this.mult,je(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 Re(){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]=Ge.newElement()),a[r][0]=e,a[r][1]=t},Ke.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)},Ke.prototype.reverse=function(){var e=new Ke;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=Ce.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 H(){et===!0?Se.searchAnimations(tt,et,nt):Se.searchAnimations()}function it(e){ie(e)}function at(e){de(e)}function ot(e){return et===!0&&(e.animationData=JSON.parse(tt)),Se.loadAnimation(e)}function st(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 U(){return typeof navigator<`u`}function ct(e,t){e===`expressions`&&oe(t)}function lt(e){switch(e){case`propertyFactory`:return V;case`shapePropertyFactory`:return Xe;case`matrix`:return Ze;default:return null}}$e.play=Se.play,$e.pause=Se.pause,$e.setLocationHref=rt,$e.togglePause=Se.togglePause,$e.setSpeed=Se.setSpeed,$e.setDirection=Se.setDirection,$e.stop=Se.stop,$e.searchAnimations=H,$e.registerAnimation=Se.registerAnimation,$e.loadAnimation=ot,$e.setSubframeRendering=it,$e.resize=Se.resize,$e.goToAndStop=Se.goToAndStop,$e.destroy=Se.destroy,$e.setQuality=st,$e.inBrowser=U,$e.installPlugin=ct,$e.freeze=Se.freeze,$e.unfreeze=Se.unfreeze,$e.setVolume=Se.setVolume,$e.mute=Se.mute,$e.unmute=Se.unmute,$e.getRegisteredAnimations=Se.getRegisteredAnimations,$e.useWebWorker=a,$e.setIDPrefix=at,$e.__getFactory=lt,$e.version=`5.13.0`;function ut(){document.readyState===`complete`&&(clearInterval(mt),H())}function dt(e){for(var t=ft.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},gt.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=ke.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=ke.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=V.getProp(e,t.p.x,0,0,this),this.py=V.getProp(e,t.p.y,0,0,this),t.p.z&&(this.pz=V.getProp(e,t.p.z,0,0,this))):this.p=V.getProp(e,t.p||{k:[0,0,0]},1,0,this),t.rx){if(this.rx=V.getProp(e,t.rx,0,D,this),this.ry=V.getProp(e,t.ry,0,D,this),this.rz=V.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)},yt.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})},K.prototype.split=function(e){if(e<=0)return[Dt(this.points[0]),this];if(e>=1)return[this,Dt(this.points[this.points.length-1])];var t=wt(this.points[0],this.points[1],e),n=wt(this.points[1],this.points[2],e),r=wt(this.points[2],this.points[3],e),i=wt(t,n,e),a=wt(n,r,e),o=wt(i,a,e);return[new K(this.points[0],t,i,o,!0),new K(o,a,r,this.points[3],!0)]};function Ot(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=Tt(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}}K.prototype.bounds=function(){return{x:Ot(this,0),y:Ot(this,1)}},K.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 kt(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[kt(t[0],e.t1,e.t),kt(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)}}K.prototype.intersections=function(e,t,n){t===void 0&&(t=2),n===void 0&&(n=7);var r=[];return Y(kt(this,0,1),kt(e,0,1),0,t,r,n),r},K.shapeSegment=function(e,t){var n=(t+1)%e.length();return new K(e.v[t],e.o[t],e.i[n],e.v[n],!0)},K.shapeSegmentInverted=function(e,t){var n=(t+1)%e.length();return new K(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 X(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 St(c[2])?null:[c[0]/c[2],c[1]/c[2]]}function Z(e,t,n){return[e[0]+Math.cos(t)*n,e[1]-Math.sin(t)*n]}function jt(e,t){return Math.hypot(e[0]-t[0],e[1]-t[1])}function Mt(e,t){return xt(e[0],t[0])&&xt(e[1],t[1])}function Nt(){}u([ht],Nt),Nt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amplitude=V.getProp(e,t.s,0,null,this),this.frequency=V.getProp(e,t.r,0,null,this),this.pointsType=V.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 Pt(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 Ft(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 It(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=Ft(i,a);return Math.atan2(0,1)-Math.atan2(o[1],o[0])}function Lt(e,t,n,r,i,a,o){var s=It(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;Pt(e,t.v[n%t._length],s,o,r,f/((i+1)*2),d/((i+1)*2),a)}function Rt(e,t,n,r,i,a){for(var o=0;o1&&t.length>1&&(i=Ht(e[0],t[t.length-1]),i)?[[e[0].split(i[0])[0]],[t[t.length-1].split(i[1])[1]]]:[n,r]}function Wt(e){for(var t,n=1;n1&&(t=Ut(e[e.length-1],e[0]),e[e.length-1]=t[0],e[0]=t[1]),e}function Gt(e,t){var n=e.inflectionPoints(),r,i,a,o;if(n.length===0)return[Bt(e,t)];if(n.length===1||xt(n[1],1))return a=e.split(n[0]),r=a[0],i=a[1],[Bt(r,t),Bt(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],[Bt(r,t),Bt(o,t),Bt(i,t)]}function Kt(){}u([ht],Kt),Kt.prototype.initModifierProperties=function(e,t){this.getValue=this.processKeys,this.amount=V.getProp(e,t.a,0,null,this),this.miterLimit=V.getProp(e,t.ml,0,null,this),this.lineJoin=t.lj,this._isAnimated=this.amount.effectsSequence.length!==0},Kt.prototype.processPath=function(e,t,n,r){var i=qe.newElement();i.c=e.c;var a=e.length();e.c||--a;var o,s,c,l=[];for(o=0;o=0;--o)c=K.shapeSegmentInverted(e,o),l.push(Gt(c,t));l=Wt(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 Yt(e){this.animationData=e}Yt.prototype.getProp=function(e){return this.animationData.slots&&this.animationData.slots[e.sid]?Object.assign(e,this.animationData.slots[e.sid].p):e};function Xt(e){return new Yt(e)}function Zt(){}Zt.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))},fn.prototype.show=function(){},fn.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},fn.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},fn.prototype.resume=function(){this._canPlay=!0},fn.prototype.setRate=function(e){this.audio.rate(e)},fn.prototype.volume=function(e){this._volumeMultiplier=e,this._previousVolume=e*this._volume,this.audio.volume(this._previousVolume)},fn.prototype.getBaseElement=function(){return null},fn.prototype.destroy=function(){},fn.prototype.sourceRectAtTime=function(){},fn.prototype.initExpressions=function(){};function pn(){}pn.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()},pn.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)}},pn.prototype.createCamera=function(){throw Error(`You're using a 3d camera. Try the html renderer.`)},pn.prototype.createAudio=function(e){return new fn(e,this.globalData,this)},pn.prototype.createFootage=function(e){return new dn(e,this.globalData,this)},pn.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)}gn.prototype.getMaskProperty=function(e){return this.viewData[e].prop},gn.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}},gn.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var _n=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}(),vn=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}(),yn={},bn=`filter_result_`;function xn(e){var t,n=`SourceGraphic`,r=e.data.ef?e.data.ef.length:0,i=ee(),a=_n.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},In.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 Un(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([ln,hn,Sn,Dn,Cn,un,wn],Un),Un.prototype.initSecondaryElement=function(){},Un.prototype.identityMatrix=new Ze,Un.prototype.buildExpressionInterface=function(){},Un.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},Un.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)}},Un.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},Gn.prototype.addEffect=function(e){this.effectsSequence.push(e),this.elem.addDynamicProperty(this)},Gn.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},Gn.prototype.buildFinalText=function(e){for(var t=[],n=0,r=e.length,i,a,o=!1,s=!1,c=``;n=55296&&i<=56319?Jt.isRegionalFlag(e,n)?c=e.substr(n,14):(a=e.charCodeAt(n+1),a>=56320&&a<=57343&&(Jt.isModifier(i,a)?(c=e.substr(n,2),o=!0):c=Jt.isFlagEmoji(e.substr(n,4))?e.substr(n,4):e.substr(n,2))):i>56319?(a=e.charCodeAt(n+1),Jt.isVariationSelector(i)&&(o=!0)):Jt.isZeroWidthJoiner(i)&&(o=!0,s=!0),o?(t[t.length-1]+=c,o=!1):t.push(c),n+=c.length;return t},Gn.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=qt(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=Ce.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([We],r);function i(e,t,n){return new r(e,t,n)}return{getTextSelectorProp:i}}();function qn(e,t,n){var r={propType:!1},i=V.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=Kn.getTextSelectorProp(e,t.s,n),this.s.t=t.s.t}function Jn(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)}Jn.prototype.searchProperties=function(){var e,t=this._textData.a.length,n,r=V.getProp;for(e=0;e=m+we||!x?(T=(m+we-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 nr(x.data,this.globalData,this);else{var w=Xn;x.data&&x.data.shapes&&(w=this.buildShapeData(x.data,n.finalSize)),C=new Un(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))}},tr.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=sr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(t);var n=sr.createCanvas(e.canvas.width,e.canvas.height);this.buffers.push(n),this.data.tt>=3&&!document._isProxy&&sr.loadLumaCanvas()}this.canvasContext=this.globalData.canvasContext,this.transformCanvas=this.globalData.transformCanvas,this.renderableEffectsManager=new lr(this),this.searchEffectTransforms()},createContent:function(){},setBlendMode:function(){var e=this.globalData;if(e.blendMode!==this.data.bm){e.blendMode=this.data.bm;var t=Qt(this.data.bm);e.canvasContext.globalCompositeOperation=t}},createRenderableComponents:function(){this.maskManager=new ur(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(mn.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=sr.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=fr[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},dr.prototype.hide=dr.prototype.hideElement,dr.prototype.show=dr.prototype.showElement;function pr(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()},mr.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},vr.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()}}},vr.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()}},vr.prototype.checkPendingElements=function(){for(;this.pendingElements.length;)this.pendingElements.pop().checkParenting()},vr.prototype.hide=function(){this.animationItem.container.style.display=`none`},vr.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([vr],Sr),Sr.prototype.createComp=function(e){return new xr(e,this.globalData,this)},ve(`canvas`,Sr),G.registerModifier(`tm`,gt),G.registerModifier(`pb`,_t),G.registerModifier(`rp`,yt),G.registerModifier(`rd`,bt),G.registerModifier(`zz`,Nt),G.registerModifier(`op`,Kt),$e}))}))(),1);function Un({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=Hn.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,H.jsx)(`div`,{className:n,ref:a,onMouseEnter:s,onMouseLeave:c})}function Wn(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function Gn(e){let t=e.toLowerCase();return t.includes(`tgsticker`)||t.includes(`lottie`)||t.includes(`json`)}function Kn({row:e}){let[t,n]=(0,g.useState)(!Gn(e.MimeType));return(0,g.useEffect)(()=>{n(!Gn(e.MimeType))},[e.DocumentID,e.MimeType]),t?(0,H.jsx)(`div`,{className:`emoji-glyph`,children:e.Alt||`🙂`}):(0,H.jsx)(Un,{className:`emoji-anim`,cacheKey:e.DocumentID,loader:()=>k.emojiAnimation(e.DocumentID),onError:()=>n(!0)})}function qn({row:e}){let{t}=U(),[n,r]=(0,g.useState)(!1);async function i(){try{await navigator.clipboard.writeText(e.DocumentID),r(!0),setTimeout(()=>r(!1),1200)}catch{}}return(0,H.jsxs)(`div`,{className:`emoji-card`,children:[(0,H.jsx)(`div`,{className:`emoji-preview`,children:(0,H.jsx)(Kn,{row:e})}),(0,H.jsxs)(`div`,{className:`emoji-meta`,children:[(0,H.jsx)(`span`,{className:`emoji-alt`,children:e.Alt||`—`}),(0,H.jsxs)(`button`,{className:`emoji-id`,type:`button`,onClick:i,title:t(`emoji.copyID`),children:[(0,H.jsx)(`span`,{className:`mono`,children:e.DocumentID}),n?(0,H.jsx)(R,{size:12}):(0,H.jsx)(me,{size:12})]}),(0,H.jsxs)(`span`,{className:`emoji-sub`,children:[e.SetTitle||t(`emoji.noSet`),` · `,Wn(e.Size)]})]})]})}function Jn(){let{t:e}=U(),[t,n]=(0,g.useState)(``),[r,i]=(0,g.useState)(null),[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 n=new URLSearchParams;t.trim()?n.set(`q`,t.trim()):e&&n.set(`before_id`,String(a));try{let e=await k.emoji(n);i(e),o(e.next_before_id)}catch(e){u(O(e))}finally{c(!1)}}(0,g.useEffect)(()=>{d(!1)},[]);let f=r?.rows??[];return(0,H.jsxs)(K,{title:e(`emoji.pageTitle`),eyebrow:r?.listing===!1?e(`emoji.queryResults`):e(`emoji.recent`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>d(!1),disabled:s,children:[(0,H.jsx)(Fe,{size:15}),` `,e(`common.refresh`)]}),children:[l&&(0,H.jsx)(J,{children:l}),(0,H.jsx)(`div`,{className:`metric-row`,children:(0,H.jsx)(X,{label:e(`emoji.currentPage`),value:String(f.length)})}),(0,H.jsx)(Ot,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),d(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:t,onChange:e=>n(e.target.value),placeholder:e(`emoji.searchPlaceholder`)})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:s,children:[s?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(Ie,{size:15}),` `,e(`common.search`)]}),r?.listing&&r.has_more&&(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>d(!0),disabled:s,children:[(0,H.jsx)(z,{size:15}),` `,e(`messages.nextPage`)]})]})}),(0,H.jsx)(`p`,{className:`about-text`,children:e(`emoji.hint`)}),f.length===0?(0,H.jsx)(`div`,{className:`empty-panel`,children:e(`common.noResults`)}):(0,H.jsx)(`div`,{className:`emoji-grid`,children:f.map(e=>(0,H.jsx)(qn,{row:e},e.DocumentID))})]})}function Yn({navigate:e}){let{t}=U();return(0,H.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,H.jsxs)(`section`,{className:`overview-band`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,H.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,H.jsxs)(`div`,{className:`overview-metrics`,children:[(0,H.jsx)(At,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,H.jsx)(At,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,H.jsx)(At,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,H.jsxs)(`div`,{className:`command-grid`,children:[(0,H.jsx)(Xn,{icon:(0,H.jsx)(et,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,H.jsx)(Xn,{icon:(0,H.jsx)(Ve,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,H.jsx)(Xn,{icon:(0,H.jsx)(De,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,H.jsxs)(`section`,{className:`work-strip`,children:[(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(I,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(Ce,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(pe,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,H.jsxs)(`div`,{className:`strip-item`,children:[(0,H.jsx)(ve,{size:16}),(0,H.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function Xn({icon:e,title:t,text:n,href:r,navigate:i}){return(0,H.jsxs)(tn,{className:`launcher`,href:r,navigate:i,children:[(0,H.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,H.jsxs)(`span`,{className:`launcher-copy`,children:[(0,H.jsx)(`strong`,{children:t}),(0,H.jsx)(`span`,{children:n})]}),(0,H.jsx)(z,{size:16})]})}function Zn({channelID:e,msgID:t,navigate:n}){let{t:r}=U(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``);async function c(){s(``);try{a(await k.groupMessage(e,t))}catch(e){s(O(e))}}if((0,g.useEffect)(()=>{c()},[e,t]),o)return(0,H.jsx)(J,{children:o});if(!i)return(0,H.jsx)(Nt,{label:r(`common.loading`)});let l=i.Message;return(0,H.jsx)(K,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,H.jsx)(ae,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:ht(l.Date)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,H.jsx)(Y,{tone:`danger`,children:r(`common.deleted`)}):(0,H.jsx)(Y,{children:r(`common.survived`)}),l.Pinned&&(0,H.jsx)(Y,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,H.jsx)(Y,{children:r(`messages.channelPost`)}),(0,H.jsxs)(Y,{children:[`pts `,l.PTS]})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,H.jsx)(Z,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,H.jsx)(Z,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,H.jsx)(Z,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,H.jsx)(Pt,{value:i.MessageJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,H.jsx)(Pt,{value:i.ChannelJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.count`)}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.messageId`)}),(0,H.jsx)(`th`,{children:r(`common.sender`)}),(0,H.jsx)(`th`,{children:r(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.PTSCount}),(0,H.jsx)(`td`,{children:e.Type}),(0,H.jsx)(`td`,{children:e.MessageID}),(0,H.jsx)(`td`,{children:e.SenderUserID}),(0,H.jsx)(`td`,{children:ht(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,H.jsx)(Mt,{colSpan:6})]})]})})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:r(`messages.eventJson`)}),(0,H.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,H.jsx)(Pt,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,H.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function Qn({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(`100`),[u,d]=(0,g.useState)(null),[f,p]=(0,g.useState)(``);async function m(e=!1){if(p(``),!n){p(t(`messages.selectChannel`));return}let r=new URLSearchParams({channel_id:String(n.ID),limit:c});if(e&&u?.rows.length){let e=u.rows[u.rows.length-1];r.set(`before_date`,String(e.Date)),r.set(`before_id`,String(e.ID)),a(String(e.Date)),s(String(e.ID))}else i&&r.set(`before_date`,i),o&&r.set(`before_id`,o);try{d(await k.groupMessages(r))}catch(e){p(O(e))}}function h(e){r(e),a(``),s(``),d(null)}let _=u?.rows??[];return(0,H.jsxs)(K,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,H.jsx)(J,{children:f}),(0,H.jsxs)(Ot,{children:[(0,H.jsx)(`div`,{className:`message-selector-grid single`,children:(0,H.jsx)(An,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,H.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,H.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,H.jsx)(Ie,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,H.jsx)(z,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(X,{label:t(`messages.currentPage`),value:String(_.length)}),(0,H.jsx)(X,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,H.jsx)(X,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,H.jsx)(X,{label:t(`messages.channelGroup`),value:n?`${n.Title||mt(n,t)} (${n.ID})`:`-`})]}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.messageId`)}),(0,H.jsx)(`th`,{children:t(`common.time`)}),(0,H.jsx)(`th`,{children:t(`common.sender`)}),(0,H.jsx)(`th`,{children:`From Peer`}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.views`)}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`messages.body`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[_.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.ID}),(0,H.jsx)(`td`,{children:ht(n.Date)}),(0,H.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,H.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.ViewsCount}),(0,H.jsx)(`td`,{children:n.Deleted?(0,H.jsx)(Y,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,H.jsx)(Y,{tone:`warn`,children:t(`messages.pinned`)}):(0,H.jsx)(Y,{children:t(`common.survived`)})}),(0,H.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,H.jsx)(z,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,H.jsx)(Mt,{colSpan:9})]})]})})]})}function $n({ownerUserID:e,msgID:t,navigate:n}){let{t:r}=U(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``);async function c(){s(``);try{a(await k.message(e,t))}catch(e){s(O(e))}}if((0,g.useEffect)(()=>{c()},[e,t]),o)return(0,H.jsx)(J,{children:o});if(!i)return(0,H.jsx)(Nt,{label:r(`common.loading`)});let l=i.Message;return(0,H.jsx)(K,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,H.jsx)(ae,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,H.jsx)(kt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:ht(l.Date)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,H.jsx)(Y,{tone:`danger`,children:r(`common.deleted`)}):(0,H.jsx)(Y,{children:r(`common.survived`)}),(0,H.jsxs)(Y,{children:[`pts `,l.PTS]}),(0,H.jsx)(Y,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,H.jsx)(Z,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,H.jsx)(Z,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,H.jsx)(Z,{label:r(`common.time`),value:ht(l.Date)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,H.jsx)(Pt,{value:i.MessageJSON})]}),(0,H.jsxs)(`div`,{className:`raw-grid`,children:[(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,H.jsx)(Pt,{value:i.DialogJSON})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,H.jsx)(Pt,{value:i.PrivateJSON})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.count`)}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.PTSCount}),(0,H.jsx)(`td`,{children:e.Type}),(0,H.jsx)(`td`,{children:ht(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,H.jsx)(Mt,{colSpan:4})]})]})})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:`ID`}),(0,H.jsx)(`th`,{children:r(`account.userID`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:r(`common.type`)}),(0,H.jsx)(`th`,{children:r(`common.status`)}),(0,H.jsx)(`th`,{children:r(`messages.attempts`)}),(0,H.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,H.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:e.ID}),(0,H.jsx)(`td`,{children:e.TargetUserID}),(0,H.jsx)(`td`,{children:e.PTS}),(0,H.jsx)(`td`,{children:e.EventType}),(0,H.jsx)(`td`,{children:e.Status}),(0,H.jsx)(`td`,{children:e.Attempts}),(0,H.jsx)(`td`,{children:G(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,H.jsx)(Mt,{colSpan:7})]})]})})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,H.jsx)(Q,{label:r(`messages.deleteThis`),icon:(0,H.jsx)(Ye,{size:15}),path:`/api/actions/delete-messages`,payload:()=>({owner_user_id:l.OwnerUserID,peer_id:l.PeerID,ids:[l.BoxID],revoke:!0}),onDone:c})]})})})}function er({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(``),[u,d]=(0,g.useState)(`100`),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(!0),[_,v]=(0,g.useState)(!1),[y,b]=(0,g.useState)(``),[x,S]=(0,g.useState)(`1`),[C,w]=(0,g.useState)(null),[T,E]=(0,g.useState)(``);async function D(e=!1){if(E(``),!n||!i){E(t(`messages.selectPrivatePeers`));return}let r=new URLSearchParams({owner_user_id:String(n.ID),peer_id:String(i.ID),limit:u});if(e&&C?.rows.length){let e=C.rows[C.rows.length-1];r.set(`before_date`,String(e.Date)),r.set(`before_id`,String(e.BoxID)),s(String(e.Date)),l(String(e.BoxID))}else o&&r.set(`before_date`,o),c&&r.set(`before_id`,c);try{w(await k.messages(r))}catch(e){E(O(e))}}function A(e){r(e),s(``),l(``),w(null)}function j(e){a(e),s(``),l(``),w(null)}return(0,H.jsxs)(K,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[T&&(0,H.jsx)(J,{children:T}),(0,H.jsxs)(Ot,{children:[(0,H.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,H.jsx)(On,{label:t(`messages.ownerUser`),value:n,onChange:A}),(0,H.jsx)(On,{label:t(`messages.peerUser`),value:i,onChange:j})]}),(0,H.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),D(!1)},children:[(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,H.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,H.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,H.jsx)(Ie,{size:15}),` `,t(`messages.searchMessages`)]}),C?.rows.length?(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>D(!0),children:[(0,H.jsx)(z,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(X,{label:t(`messages.currentPage`),value:String(C?.rows.length??0)}),(0,H.jsx)(X,{label:t(`messages.deleted`),value:String((C?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,H.jsx)(X,{label:t(`messages.outgoing`),value:String((C?.rows??[]).filter(e=>e.Outgoing).length)}),(0,H.jsx)(X,{label:t(`messages.ownerPeer`),value:n&&i?`${pt(n)} / ${pt(i)}`:`-`})]}),(0,H.jsxs)(`div`,{className:`operation-row`,children:[(0,H.jsxs)(`div`,{className:`operation-box`,children:[(0,H.jsxs)(`div`,{className:`operation-title`,children:[(0,H.jsx)(Ye,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,H.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,H.jsx)(Q,{path:`/api/actions/delete-messages`,label:t(`messages.previewDelete`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,ids:Dt(f,t(`messages.msgIDsInvalid`)),revoke:m})})]}),(0,H.jsxs)(`div`,{className:`operation-box`,children:[(0,H.jsxs)(`div`,{className:`operation-title`,children:[(0,H.jsx)(Se,{size:15}),` `,t(`messages.clearHistory`)]}),(0,H.jsx)(`input`,{value:y,onChange:e=>b(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,H.jsx)(`input`,{value:x,onChange:e=>S(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,H.jsx)(Q,{path:`/api/actions/delete-history`,label:t(`messages.previewClearHistory`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,max_id:_t(y),max_batches:_t(x),just_clear:_,revoke:m})})]})]}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.messageId`)}),(0,H.jsx)(`th`,{children:t(`common.time`)}),(0,H.jsx)(`th`,{children:t(`common.sender`)}),(0,H.jsx)(`th`,{children:t(`messages.direction`)}),(0,H.jsx)(`th`,{children:`PTS`}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`messages.body`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[C?.rows.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,H.jsx)(`td`,{children:ht(n.Date)}),(0,H.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,H.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,H.jsx)(`td`,{children:n.PTS}),(0,H.jsx)(`td`,{children:n.Deleted?(0,H.jsx)(Y,{tone:`danger`,children:t(`common.deleted`)}):(0,H.jsx)(Y,{children:t(`common.survived`)})}),(0,H.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,H.jsx)(z,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!C||C.rows.length===0)&&(0,H.jsx)(Mt,{colSpan:8})]})]})})]})}var tr=0,nr=e=>`${e}-${++tr}`,rr=[{center:`#6f5bea`,edge:`#34278f`,pattern:`#a89df5`,text:`#ffffff`},{center:`#32a86b`,edge:`#17613e`,pattern:`#8ee0b3`,text:`#ffffff`},{center:`#df8d2f`,edge:`#8c421e`,pattern:`#ffd08a`,text:`#ffffff`},{center:`#d95878`,edge:`#7b2944`,pattern:`#f5a1b6`,text:`#ffffff`}];function ir(e){if(!e.length)return e;let t=Math.floor(1e3/e.length),n=1e3%e.length;return e.map((e,r)=>({...e,rarity:String(t+ +(r({key:nr(e),name:``,rarity:`1`,sortOrder:String(t),file:null,animation:null,fileError:``});function or(e){let t=e.reduce((e,t)=>{let n=Number(t.backdropID);return Number.isInteger(n)?Math.max(e,n):e},0)+1,n=rr[e.length%rr.length];return{key:nr(`backdrop`),name:``,backdropID:String(t),rarity:`1`,sortOrder:String(e.length),...n}}var sr=e=>ir([ar(e,0),ar(e,1)]),cr=()=>{let e=or([]);return ir([e,or([e])])};function lr({data:e,compact:t=!1}){let n=(0,g.useRef)(null);return(0,g.useEffect)(()=>{if(!n.current)return;let t=Hn.default.loadAnimation({container:n.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)});return()=>t.destroy()},[e]),(0,H.jsx)(`div`,{className:`collectible-animation ${t?`compact`:``}`,ref:n})}function ur({giftID:e,attribute:t}){let[n,r]=(0,g.useState)(null),[i,a]=(0,g.useState)(!1);return(0,g.useEffect)(()=>{let n=!1;return a(!1),k.giftCollectibleAnimation(e,t.kind,t.id).then(e=>{n||r(e)}).catch(()=>{n||a(!0)}),()=>{n=!0}},[e,t.id,t.kind]),i?(0,H.jsx)(`div`,{className:`collectible-animation compact failed`,children:`!`}):n?(0,H.jsx)(lr,{data:n,compact:!0}):(0,H.jsx)(`div`,{className:`collectible-animation compact loading`,children:(0,H.jsx)(L,{className:`spin`,size:15})})}async function dr(e){let t=new Uint8Array(await e.arrayBuffer()),n=t;if(t.length>=2&&t[0]===31&&t[1]===139){if(!(`DecompressionStream`in window))throw Error(`This browser cannot preview TGS files`);let e=new Blob([t]).stream().pipeThrough(new DecompressionStream(`gzip`));n=new Uint8Array(await new Response(e).arrayBuffer())}let r=JSON.parse(new TextDecoder().decode(n));if(!r||typeof r!=`object`||Array.isArray(r))throw Error(`Invalid Lottie JSON`);return r}var fr=e=>Number.parseInt(e.replace(`#`,``),16),pr=e=>e.rarity_kind===`permille`?`${e.rarity_permille}‰`:e.rarity_kind;function mr({gift:e,onClose:t,onPublished:n}){let{t:r}=U(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(!0),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(null),[m,h]=(0,g.useState)(`100`),[_,v]=(0,g.useState)(`1000`),[y,b]=(0,g.useState)(`gift-${e.GiftID}`),[x,S]=(0,g.useState)(``),[C,w]=(0,g.useState)(()=>sr(`model`)),[T,E]=(0,g.useState)(()=>sr(`pattern`)),[D,A]=(0,g.useState)(cr);(0,g.useEffect)(()=>{let t=!1;return k.giftCollectibles(e.GiftID).then(n=>{t||(a(n),n.found&&(h(String(n.upgrade_stars??100)),v(String(n.supply_total??1e3)),b(n.slug_prefix??`gift-${e.GiftID}`)))}).catch(e=>d(O(e))).finally(()=>{t||s(!1)}),()=>{t=!0}},[e.GiftID]);let j=(0,g.useMemo)(()=>({models:C.reduce((e,t)=>e+Number(t.rarity||0),0),patterns:T.reduce((e,t)=>e+Number(t.rarity||0),0),backdrops:D.reduce((e,t)=>e+Number(t.rarity||0),0)}),[C,T,D]),M=()=>p(null),N=(e,t,n)=>{(e===`models`?w:E)(e=>e.map(e=>e.key===t?{...e,...n}:e)),M()};async function P(e,t,n){if(N(e,t.key,{file:n,animation:null,fileError:``}),n)try{let r=await dr(n);N(e,t.key,{animation:r,fileError:``})}catch(n){N(e,t.key,{animation:null,fileError:O(n)})}}function F(e,t=``){if(!x.trim())throw Error(r(`action.reasonRequired`));if(C.length<2||T.length<2||D.length<2)throw Error(r(`collectibles.minimumAttributes`));let n=D.map(e=>Number(e.backdropID));if(new Set(n).size!==n.length)throw Error(r(`collectibles.duplicateBackdropID`));for(let e of[...C,...T])if(!e.file)throw Error(r(`collectibles.fileRequired`));let i=new FormData,a=e=>e.map(e=>({name:e.name.trim(),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),file_key:e.key}));i.set(`metadata`,JSON.stringify({command_id:t,reason:x.trim(),confirm:e,upgrade_stars:m,supply_total:Number(_),slug_prefix:y.trim().toLowerCase(),models:a(C),patterns:a(T),backdrops:D.map(e=>({name:e.name.trim(),backdrop_id:Number(e.backdropID),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),center_color:fr(e.center),edge_color:fr(e.edge),pattern_color:fr(e.pattern),text_color:fr(e.text)}))}));for(let e of[...C,...T])i.set(e.key,e.file,e.file.name);return i}async function ee(){l(!0),d(``),p(null);try{p(await k.publishGiftCollectibles(e.GiftID,F(!1)))}catch(e){d(O(e))}finally{l(!1)}}async function te(){if(f){l(!0),d(``);try{await k.publishGiftCollectibles(e.GiftID,F(!0,f.command_id)),n(),t()}catch(e){d(O(e))}finally{l(!1)}}}let ne=(e,t,n)=>(0,H.jsxs)(`section`,{className:`collectible-section`,children:[(0,H.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.${e}`)}),(0,H.jsx)(`span`,{children:r(`collectibles.rarityHint`)})]}),(0,H.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,H.jsxs)(Y,{tone:j[e]>0?`good`:`neutral`,children:[j[e],`‰`]}),(0,H.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{n(ir([...t,ar(e===`models`?`model`:`pattern`,t.length)])),M()},children:[(0,H.jsx)(Me,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,H.jsx)(`div`,{className:`collectible-rows`,children:t.map((i,a)=>(0,H.jsxs)(`div`,{className:`collectible-row animated`,children:[(0,H.jsx)(`div`,{className:`collectible-row-index`,children:a+1}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`common.name`)}),(0,H.jsx)(`input`,{value:i.name,maxLength:128,onChange:t=>N(e,i.key,{name:t.target.value})})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:i.rarity,onChange:t=>N(e,i.key,{rarity:t.target.value})})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:i.sortOrder,onChange:t=>N(e,i.key,{sortOrder:t.target.value})})]}),(0,H.jsxs)(`label`,{className:`collectible-file`,children:[(0,H.jsx)(`span`,{children:r(`gifts.animation`)}),(0,H.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:t=>void P(e,i,t.target.files?.[0]??null)}),(0,H.jsxs)(`em`,{children:[(0,H.jsx)(_e,{size:13}),i.file?.name??r(`gifts.chooseFile`)]})]}),(0,H.jsx)(`div`,{className:`collectible-inline-preview`,children:i.animation?(0,H.jsx)(lr,{data:i.animation,compact:!0}):(0,H.jsx)(re,{size:16})}),(0,H.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:t.length<=2,onClick:()=>{n(ir(t.filter(e=>e.key!==i.key))),M()},"aria-label":r(`collectibles.remove`),children:(0,H.jsx)(Ye,{size:14})}),i.fileError&&(0,H.jsx)(`span`,{className:`collectible-file-error`,children:i.fileError})]},i.key))})]});return(0,sn.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal collectible-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":r(`collectibles.title`,{id:e.GiftID}),children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:r(`collectibles.eyebrow`)}),(0,H.jsx)(`h2`,{children:r(`collectibles.title`,{id:e.GiftID})}),(0,H.jsx)(`p`,{children:e.Title||`Gift #${e.GiftID}`})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:c,"aria-label":r(`action.close`),children:(0,H.jsx)(nt,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body collectible-modal-body`,children:[o?(0,H.jsxs)(`div`,{className:`collectible-loading`,children:[(0,H.jsx)(L,{className:`spin`}),r(`common.loading`)]}):i?.found?(0,H.jsxs)(`section`,{className:`collectible-active`,children:[(0,H.jsxs)(`div`,{className:`collectible-active-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(be,{size:18}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.activeRevision`,{revision:i.revision??0})}),(0,H.jsxs)(`span`,{children:[i.slug_prefix,` · ⭐ `,i.upgrade_stars,` · `,i.issued,` / `,i.supply_total]})]})]}),(0,H.jsx)(Y,{tone:`good`,children:r(`collectibles.published`)})]}),(0,H.jsxs)(`div`,{className:`collectible-active-grid`,children:[[...i.models??[],...i.patterns??[]].map(t=>(0,H.jsxs)(`article`,{children:[(0,H.jsx)(ur,{giftID:e.GiftID,attribute:t}),(0,H.jsxs)(`div`,{children:[(0,H.jsxs)(`strong`,{children:[t.name,t.crafted&&(0,H.jsx)(Y,{children:`crafted`})]}),(0,H.jsxs)(`span`,{children:[r(`collectibles.${t.kind}`),` · `,pr(t)]})]})]},`${t.kind}-${t.id}`)),(i.backdrops??[]).map(e=>(0,H.jsxs)(`article`,{children:[(0,H.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, #${(e.center_color??0).toString(16).padStart(6,`0`)}, #${(e.edge_color??0).toString(16).padStart(6,`0`)})`,color:`#${(e.text_color??16777215).toString(16).padStart(6,`0`)}`},children:`Aa`}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e.name}),(0,H.jsxs)(`span`,{children:[r(`collectibles.backdrop`),` · `,pr(e)]})]})]},`backdrop-${e.id}`))]})]}):(0,H.jsxs)(`div`,{className:`collectible-empty`,children:[(0,H.jsx)(be,{size:22}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.noPool`)}),(0,H.jsx)(`span`,{children:r(`collectibles.noPoolHint`)})]})]}),(0,H.jsxs)(`section`,{className:`collectible-definition`,children:[(0,H.jsxs)(`div`,{className:`collectible-definition-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.publishNew`)}),(0,H.jsx)(`span`,{children:r(`collectibles.immutableHint`)})]}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,H.jsx)(`span`,{children:`TGS`}),(0,H.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,H.jsxs)(`div`,{className:`gift-fields-grid collectible-main-fields`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.upgradeStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:m,onChange:e=>{h(e.target.value),M()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.supply`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:_,onChange:e=>{v(e.target.value),M()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.slug`)}),(0,H.jsx)(`input`,{value:y,maxLength:48,onChange:e=>{b(e.target.value.toLowerCase()),M()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.reason`)}),(0,H.jsx)(`input`,{value:x,maxLength:1e3,placeholder:r(`gifts.reasonPlaceholder`),onChange:e=>S(e.target.value)})]})]}),ne(`models`,C,w),ne(`patterns`,T,E),(0,H.jsxs)(`section`,{className:`collectible-section`,children:[(0,H.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.backdrops`)}),(0,H.jsx)(`span`,{children:r(`collectibles.colorHint`)})]}),(0,H.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,H.jsxs)(Y,{tone:j.backdrops>0?`good`:`neutral`,children:[j.backdrops,`‰`]}),(0,H.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{A(ir([...D,or(D)])),M()},children:[(0,H.jsx)(Me,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,H.jsx)(`div`,{className:`collectible-rows`,children:D.map((e,t)=>(0,H.jsxs)(`div`,{className:`collectible-row backdrop`,children:[(0,H.jsx)(`div`,{className:`collectible-row-index`,children:t+1}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`common.name`)}),(0,H.jsx)(`input`,{value:e.name,maxLength:128,onChange:t=>{A(D.map(n=>n.key===e.key?{...n,name:t.target.value}:n)),M()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.backdropID`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:e.backdropID,onChange:t=>{A(D.map(n=>n.key===e.key?{...n,backdropID:t.target.value}:n)),M()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:e.rarity,onChange:t=>{A(D.map(n=>n.key===e.key?{...n,rarity:t.target.value}:n)),M()}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:e.sortOrder,onChange:t=>{A(D.map(n=>n.key===e.key?{...n,sortOrder:t.target.value}:n)),M()}})]}),[`center`,`edge`,`pattern`,`text`].map(t=>(0,H.jsxs)(`label`,{className:`collectible-color`,children:[(0,H.jsx)(`span`,{children:r(`collectibles.color.${t}`)}),(0,H.jsx)(`input`,{type:`color`,value:e[t],onChange:n=>{A(D.map(r=>r.key===e.key?{...r,[t]:n.target.value}:r)),M()}})]},t)),(0,H.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, ${e.center}, ${e.edge})`,color:e.text},children:`Aa`}),(0,H.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:D.length<=2,onClick:()=>{A(ir(D.filter(t=>t.key!==e.key))),M()},"aria-label":r(`collectibles.remove`),children:(0,H.jsx)(Ye,{size:14})})]},e.key))})]})]}),u&&(0,H.jsx)(J,{children:u}),f&&(0,H.jsxs)(`div`,{className:`gift-validation`,children:[(0,H.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,H.jsx)(I,{size:17}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:r(`collectibles.validationReady`)}),(0,H.jsx)(`span`,{children:r(`collectibles.validationHint`)})]})]}),(0,H.jsx)(`pre`,{children:JSON.stringify(f.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:c,children:r(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:ee,disabled:c,children:[c?(0,H.jsx)(L,{className:`spin`,size:15}):(0,H.jsx)(Ve,{size:15}),r(`gifts.validate`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:te,disabled:c||!f,children:[(0,H.jsx)(Qe,{size:15}),r(`collectibles.publish`)]})]})]})}),document.body)}function hr(e){return e.model_count+e.pattern_count+e.backdrop_count}function gr(e){let t=Number(e);return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/(1024*1024)).toFixed(1)} MB`}function _r({giftID:e,revision:t,compact:n=!1}){let r=(0,g.useRef)(null),i=(0,g.useRef)(null),[a,o]=(0,g.useState)(!0),[s,c]=(0,g.useState)(``);(0,g.useEffect)(()=>{let t=!1;return k.giftAnimation(e).then(e=>{t||!r.current||(i.current?.destroy(),i.current=Hn.default.loadAnimation({container:r.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(e=>c(O(e))),()=>{t=!0,i.current?.destroy(),i.current=null}},[e,t]);function l(){i.current&&(a?i.current.pause():i.current.play(),o(!a))}return(0,H.jsxs)(`div`,{className:`gift-animation-shell ${n?`compact`:``}`,children:[(0,H.jsx)(`div`,{className:`gift-animation`,ref:r,children:s&&(0,H.jsx)(`span`,{children:s})}),(0,H.jsx)(`button`,{className:`gift-play`,type:`button`,onClick:l,"aria-label":a?`Pause`:`Play`,children:a?(0,H.jsx)(Ae,{size:14}):(0,H.jsx)(je,{size:14})})]})}function vr({sourceGiftID:e}){let t=(0,g.useRef)(null);return(0,g.useEffect)(()=>{let n=!1,r=null;return k.officialGiftAnimation(e).then(e=>{n||!t.current||(r=Hn.default.loadAnimation({container:t.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)}))}).catch(()=>void 0),()=>{n=!0,r?.destroy()}},[e]),(0,H.jsx)(`div`,{className:`gift-animation-shell`,children:(0,H.jsx)(`div`,{className:`gift-animation`,ref:t})})}function yr(){let{t:e}=U(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(!1),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(null),[d,f]=(0,g.useState)(`official`),[p,m]=(0,g.useState)([]),[h,_]=(0,g.useState)(``),[v,y]=(0,g.useState)(`all`),[b,x]=(0,g.useState)(``),[S,C]=(0,g.useState)(!0),[w,T]=(0,g.useState)(`0`),[E,D]=(0,g.useState)(`0`),[A,j]=(0,g.useState)(``),[M,N]=(0,g.useState)(`0`),[P,F]=(0,g.useState)(``),[ee,te]=(0,g.useState)(`50`),[ne,re]=(0,g.useState)(`50`),[ie,ae]=(0,g.useState)(`0`),[oe,se]=(0,g.useState)(!0),[ce,le]=(0,g.useState)(``),[ue,de]=(0,g.useState)(null),[R,fe]=(0,g.useState)(!1),[z,pe]=(0,g.useState)(``),[me,he]=(0,g.useState)(``);async function ge(){pe(``);try{n((await k.gifts()).Gifts??[])}catch(e){pe(O(e))}}(0,g.useEffect)(()=>{ge()},[]),(0,g.useEffect)(()=>{!a||d!==`official`||p.length>0||k.officialGifts().then(e=>m(e.gifts??[])).catch(e=>he(O(e)))},[a,d,p.length]);let ve=(0,g.useMemo)(()=>p.find(e=>e.source_gift_id===b)??null,[p,b]),ye=(0,g.useMemo)(()=>({all:p.length,upgrade:p.filter(e=>e.can_upgrade).length,craft:p.filter(e=>e.can_craft).length,basic:p.filter(e=>!e.can_upgrade).length}),[p]),xe=(0,g.useMemo)(()=>{let e=h.trim().toLowerCase();return p.filter(t=>(v===`all`||v===`upgrade`&&t.can_upgrade||v===`craft`&&t.can_craft||v===`basic`&&!t.can_upgrade)&&(!e||t.source_gift_id.includes(e)||t.title.toLowerCase().includes(e)))},[p,h,v]),B=(0,g.useMemo)(()=>{let e=r.trim().toLowerCase();return e?t.filter(t=>String(t.GiftID).includes(e)||t.Title.toLowerCase().includes(e)||t.SourceFormat.toLowerCase().includes(e)):t},[t,r]);function Se(t,n=``){if(!l)throw Error(e(`gifts.fileRequired`));if(!ce.trim())throw Error(e(`action.reasonRequired`));let r=new FormData;return r.set(`metadata`,JSON.stringify({command_id:n,reason:ce.trim(),confirm:t,gift_id:M,title:P.trim(),stars:ee,convert_stars:ne,enabled:oe,sort_order:Number(ie)})),r.set(`file`,l,l.name),r}function Ce(t,n=``){if(!b)throw Error(e(`gifts.officialRequired`));if(!ce.trim())throw Error(e(`action.reasonRequired`));return{command_id:n,reason:ce.trim(),confirm:t,source_gift_id:b,gift_id:M,title:P.trim(),stars:ee,convert_stars:ne,enabled:oe,sort_order:Number(ie),include_collectible:S,upgrade_stars:w,supply_total:Number(E),slug_prefix:A.trim().toLowerCase()}}function we(t){x(t.source_gift_id),F(t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})),te(String(t.stars)),re(String(t.convert_stars)),C(t.can_upgrade),T(t.upgrade_stars),D(String(t.availability_total||1)),j(`official-${t.source_gift_id}`),de(null)}async function Te(){fe(!0),he(``),de(null);try{de(d===`official`?await k.importOfficialGift(Ce(!1)):await k.importGift(Se(!1)))}catch(e){he(O(e))}finally{fe(!1)}}async function Ee(){if(ue){fe(!0),he(``);try{d===`official`?await k.importOfficialGift(Ce(!0,ue.command_id)):await k.importGift(Se(!0,ue.command_id)),de(null),u(null),N(`0`),F(``),x(``),await ge(),o(!1)}catch(e){he(O(e))}finally{fe(!1)}}}function De(){N(`0`),F(``),te(`50`),re(`50`),ae(`0`),se(!0),le(``),u(null),de(null),he(``),f(`official`),x(``),_(``),y(`all`),o(!0)}function Oe(e){N(e.GiftID),F(e.Title),te(String(e.Stars)),re(String(e.ConvertStars)),ae(String(e.SortOrder)),se(e.Enabled),le(``),u(null),de(null),he(``),f(`official`),x(``),_(``),y(`all`),o(!0)}return(0,H.jsxs)(K,{title:e(`gifts.pageTitle`),eyebrow:e(`gifts.eyebrow`),actions:(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>ge(),disabled:R,children:[(0,H.jsx)(Fe,{size:15}),` `,e(`common.refresh`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:De,children:[(0,H.jsx)(Me,{size:15}),` `,e(`gifts.add`)]})]}),children:[z&&(0,H.jsx)(J,{children:z}),(0,H.jsxs)(`div`,{className:`metric-row gift-metrics`,children:[(0,H.jsx)(X,{label:e(`gifts.total`),value:String(t.length)}),(0,H.jsx)(X,{label:e(`gifts.enabled`),value:String(t.filter(e=>e.Enabled).length),tone:`good`}),(0,H.jsx)(X,{label:e(`gifts.received`),value:t.reduce((e,t)=>e+BigInt(t.ReceivedCount),0n).toString()}),(0,H.jsx)(X,{label:e(`gifts.formats`),value:`TGS / Lottie`})]}),(0,H.jsx)(Ot,{children:(0,H.jsxs)(`div`,{className:`toolbar`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`gifts.searchPlaceholder`)})]}),(0,H.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:B.length,total:t.length})})]})}),(0,H.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table gift-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:e(`gifts.animation`)}),(0,H.jsx)(`th`,{children:e(`gifts.idRevision`)}),(0,H.jsx)(`th`,{children:e(`gifts.title`)}),(0,H.jsx)(`th`,{children:e(`gifts.price`)}),(0,H.jsx)(`th`,{children:e(`gifts.source`)}),(0,H.jsx)(`th`,{children:e(`gifts.received`)}),(0,H.jsx)(`th`,{children:e(`common.status`)}),(0,H.jsx)(`th`,{children:e(`common.updatedAt`)}),(0,H.jsx)(`th`,{children:e(`common.actions`)})]})}),(0,H.jsxs)(`tbody`,{children:[B.map(t=>(0,H.jsxs)(`tr`,{className:t.Enabled?``:`gift-row-disabled`,children:[(0,H.jsx)(`td`,{children:(0,H.jsx)(_r,{giftID:t.GiftID,revision:t.Revision,compact:!0})}),(0,H.jsxs)(`td`,{className:`mono`,children:[t.GiftID,` / `,t.Revision]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`strong`,{className:`gift-table-title`,children:t.Title||`Gift #${t.GiftID}`}),(0,H.jsxs)(`span`,{className:`gift-sort-order`,children:[e(`gifts.sortOrder`),`: `,t.SortOrder]})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsxs)(`strong`,{className:`gift-table-price`,children:[`⭐ `,t.Stars]}),(0,H.jsxs)(`span`,{className:`gift-convert-price`,children:[`→ `,t.ConvertStars]})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(Y,{children:t.SourceFormat}),(0,H.jsx)(`span`,{className:`gift-source-size`,children:gr(t.AnimationSize)})]}),(0,H.jsx)(`td`,{children:t.ReceivedCount}),(0,H.jsx)(`td`,{children:(0,H.jsx)(Y,{tone:t.Enabled?`good`:`neutral`,children:t.Enabled?e(`common.enabled`):e(`common.disabled`)})}),(0,H.jsx)(`td`,{children:G(t.UpdatedAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,H.jsxs)(`button`,{className:`btn compact-btn collectible-button`,type:`button`,onClick:()=>c(t),children:[(0,H.jsx)(be,{size:13}),e(`collectibles.manage`)]}),(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>Oe(t),children:e(`gifts.replace`)}),(0,H.jsx)(Q,{compact:!0,tone:`neutral`,label:t.Enabled?e(`gifts.disable`):e(`gifts.enable`),path:`/api/actions/set-gift-enabled`,payload:()=>({gift_id:t.GiftID,enabled:!t.Enabled}),onDone:()=>void ge()})]})})]},t.GiftID)),B.length===0&&(0,H.jsx)(Mt,{colSpan:9})]})]})}),a&&(0,sn.createPortal)((0,H.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,H.jsxs)(`section`,{className:`modal command-modal gift-import-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":M===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:M}),children:[(0,H.jsxs)(`div`,{className:`modal-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`eyebrow`,children:e(`gifts.importEyebrow`)}),(0,H.jsx)(`h2`,{children:M===`0`?e(`gifts.importTitle`):e(`gifts.newRevision`,{id:M})})]}),(0,H.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>o(!1),disabled:R,"aria-label":e(`action.close`),children:(0,H.jsx)(nt,{size:15})})]}),(0,H.jsxs)(`div`,{className:`command-body gift-import-modal-body`,children:[(0,H.jsxs)(`div`,{className:`command-steps`,children:[(0,H.jsxs)(`div`,{className:`command-step ${(d===`official`?b:l)?`done`:`active`}`,children:[(0,H.jsx)(`span`,{children:`1`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepDetails`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${ue?`done`:(d===`official`?b:l)?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`2`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepValidate`)})]}),(0,H.jsxs)(`div`,{className:`command-step ${ue?`active`:``}`,children:[(0,H.jsx)(`span`,{children:`3`}),(0,H.jsx)(`strong`,{children:e(`gifts.stepImport`)})]})]}),(0,H.jsxs)(`div`,{className:`gift-source-tabs`,children:[(0,H.jsx)(`button`,{className:`btn ${d===`official`?`primary`:``}`,type:`button`,onClick:()=>{f(`official`),de(null)},children:e(`gifts.officialSource`)}),(0,H.jsx)(`button`,{className:`btn ${d===`file`?`primary`:``}`,type:`button`,onClick:()=>{f(`file`),de(null)},children:e(`gifts.fileSource`)})]}),d===`official`?(0,H.jsxs)(`section`,{className:`official-gift-picker`,children:[(0,H.jsxs)(`div`,{className:`gift-import-note`,children:[(0,H.jsx)(`span`,{children:e(`gifts.officialHint`)}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,H.jsx)(`span`,{children:p.length}),(0,H.jsx)(`span`,{children:`SHA-256`})]})]}),(0,H.jsxs)(`div`,{className:`official-gift-tools`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:h,onChange:e=>_(e.target.value),placeholder:e(`gifts.officialSearch`)})]}),(0,H.jsx)(`span`,{children:e(`gifts.officialResults`,{shown:xe.length,total:p.length})})]}),(0,H.jsx)(`div`,{className:`official-gift-categories`,role:`group`,"aria-label":e(`gifts.officialCategoryLabel`),children:[`all`,`upgrade`,`craft`,`basic`].map(t=>(0,H.jsxs)(`button`,{className:v===t?`active`:``,type:`button`,"aria-pressed":v===t,onClick:()=>y(t),children:[e(`gifts.officialCategory.${t}`),(0,H.jsx)(`span`,{children:ye[t]})]},t))}),(0,H.jsxs)(`div`,{className:`official-gift-list`,role:`listbox`,"aria-label":e(`gifts.officialSelect`),children:[xe.map(t=>{let n=t.source_gift_id===b;return(0,H.jsxs)(`button`,{className:`official-gift-option ${n?`selected`:``}`,type:`button`,role:`option`,"aria-selected":n,onClick:()=>we(t),children:[(0,H.jsxs)(`span`,{className:`official-gift-option-head`,children:[(0,H.jsx)(`strong`,{children:t.title||e(`gifts.officialUnnamed`,{id:t.source_gift_id})}),(0,H.jsxs)(`span`,{className:`mono`,children:[`#`,t.source_gift_id]})]}),(0,H.jsxs)(`span`,{className:`official-gift-option-meta`,children:[(0,H.jsxs)(`span`,{children:[`⭐ `,t.stars]}),(0,H.jsx)(`span`,{children:e(`gifts.officialAttributes`,{count:hr(t)})})]}),(0,H.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,H.jsx)(`span`,{className:t.can_upgrade?`yes`:`no`,children:t.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,H.jsx)(`span`,{className:t.can_craft?`craft`:`no`,children:t.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]},t.source_gift_id)}),xe.length===0&&(0,H.jsx)(`div`,{className:`official-gift-empty`,children:e(`gifts.officialEmpty`)})]}),ve&&(0,H.jsxs)(`div`,{className:`official-gift-selected`,children:[(0,H.jsx)(vr,{sourceGiftID:ve.source_gift_id}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:ve.title||e(`gifts.officialUnnamed`,{id:ve.source_gift_id})}),(0,H.jsx)(`span`,{className:`mono`,children:ve.source_gift_id}),(0,H.jsxs)(`small`,{children:[ve.model_count,` `,e(`collectibles.models`),` · `,ve.pattern_count,` `,e(`collectibles.patterns`),` · `,ve.backdrop_count,` `,e(`collectibles.backdrops`)]}),(0,H.jsxs)(`span`,{className:`official-gift-capabilities`,children:[(0,H.jsx)(`span`,{className:ve.can_upgrade?`yes`:`no`,children:ve.can_upgrade?e(`gifts.canUpgrade`):e(`gifts.cannotUpgrade`)}),(0,H.jsx)(`span`,{className:ve.can_craft?`craft`:`no`,children:ve.can_craft?e(`gifts.canCraft`):e(`gifts.cannotCraft`)})]})]})]}),ve?.can_upgrade&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:S,onChange:e=>{C(e.target.checked),de(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:e(`gifts.includeCollectible`)})]}),S&&(0,H.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.upgradeStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:w,onChange:e=>{T(e.target.value),de(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.supply`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:E,onChange:e=>{D(e.target.value),de(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`collectibles.slug`)}),(0,H.jsx)(`input`,{value:A,maxLength:48,onChange:e=>{j(e.target.value.toLowerCase()),de(null)}})]})]})]})]}):(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`div`,{className:`gift-import-note`,children:[(0,H.jsx)(`span`,{children:e(`gifts.importHint`)}),(0,H.jsxs)(`div`,{className:`gift-format-chips`,"aria-label":e(`gifts.formats`),children:[(0,H.jsx)(`span`,{children:`TGS`}),(0,H.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,H.jsxs)(`label`,{className:`gift-file-picker ${l?`has-file`:``}`,children:[(0,H.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:e=>{u(e.target.files?.[0]??null),de(null)}}),(0,H.jsx)(`span`,{className:`gift-file-icon`,children:(0,H.jsx)(_e,{size:22})}),(0,H.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,H.jsx)(`span`,{className:`gift-field-label`,children:e(`gifts.animation`)}),(0,H.jsx)(`strong`,{children:l?l.name:e(`gifts.filePrompt`)}),(0,H.jsx)(`small`,{children:l?gr(l.size):e(`gifts.fileHint`)})]}),(0,H.jsx)(`span`,{className:`gift-file-action`,children:e(l?`gifts.changeFile`:`gifts.chooseFile`)})]})]}),(0,H.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.title`)}),(0,H.jsx)(`input`,{value:P,maxLength:128,placeholder:e(`gifts.titlePlaceholder`),onChange:e=>{F(e.target.value),de(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.stars`)}),(0,H.jsx)(`input`,{type:`number`,min:`1`,value:ee,onChange:e=>{te(e.target.value),de(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.convertStars`)}),(0,H.jsx)(`input`,{type:`number`,min:`0`,value:ne,onChange:e=>{re(e.target.value),de(null)}})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:e(`gifts.sortOrder`)}),(0,H.jsx)(`input`,{type:`number`,value:ie,onChange:e=>{ae(e.target.value),de(null)}})]})]}),(0,H.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,H.jsx)(`span`,{children:e(`gifts.reason`)}),(0,H.jsx)(`input`,{value:ce,placeholder:e(`gifts.reasonPlaceholder`),onChange:e=>le(e.target.value)})]}),(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:oe,onChange:e=>{se(e.target.checked),de(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:e(`gifts.enableAfterImport`)})]}),me&&(0,H.jsx)(J,{children:me}),ue&&(0,H.jsxs)(`div`,{className:`gift-validation`,children:[(0,H.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,H.jsx)(I,{size:17}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e(`gifts.validationReady`)}),(0,H.jsx)(`span`,{children:e(`gifts.validationHint`)})]})]}),(0,H.jsx)(`pre`,{children:JSON.stringify(ue.details,null,2)})]})]}),(0,H.jsxs)(`div`,{className:`modal-actions`,children:[(0,H.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>o(!1),disabled:R,children:e(`common.close`)}),(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:Te,disabled:R,children:[R?(0,H.jsx)(L,{className:`spin`,size:15}):(0,H.jsx)(Ve,{size:15}),e(`gifts.validate`)]}),(0,H.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:Ee,disabled:R||!ue,children:[(0,H.jsx)(Qe,{size:15}),e(`gifts.confirmImport`)]})]})]})}),document.body),s&&(0,H.jsx)(mr,{gift:s,onClose:()=>c(null),onPublished:()=>void ge()})]})}var br=`777000`;function xr(e){let t=e.rarity_permille>0?` · ${(e.rarity_permille/10).toFixed(1)}%`:``;return`${e.name||`#${e.id}`}${t}`}function Sr({gift:e,onDone:t}){let{t:n}=U(),[r,i]=(0,g.useState)(`user`),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(null),[l,u]=(0,g.useState)(``),[d,f]=(0,g.useState)(!1),[p,m]=(0,g.useState)(!1),[h,_]=(0,g.useState)(null),[v,y]=(0,g.useState)(``),[b,x]=(0,g.useState)(`0`),[S,C]=(0,g.useState)(`0`),[w,T]=(0,g.useState)(`0`),[E,D]=(0,g.useState)(``),[A,j]=(0,g.useState)(null),[M,N]=(0,g.useState)(``),[P,F]=(0,g.useState)(!1),te=r===`user`?a?.ID??0:s?.ID??0,ne=r===`user`&&p;(0,g.useEffect)(()=>{m(!1),_(null),y(``),x(`0`),C(`0`),T(`0`),j(null),N(``)},[e.GiftID]),(0,g.useEffect)(()=>{if(!ne||h)return;let t=!1;return y(``),k.giftCollectibles(e.GiftID).then(e=>{t||_(e)}).catch(e=>{t||y(O(e))}),()=>{t=!0}},[ne,h,e.GiftID]);function re(t){return{gift_id:e.GiftID,sender_user_id:Number(br),user_id:r===`user`?te:0,channel_id:r===`channel`?te:0,hide_name:d,message:l.trim(),upgrade:ne,model_attribute_id:ne?b:`0`,pattern_attribute_id:ne?S:`0`,backdrop_attribute_id:ne?w:`0`,reason:E.trim(),confirm:t}}let ie=(0,g.useMemo)(()=>re(!1),[e.GiftID,r,te,l,d,p,b,S,w,E]),ae=A?.dry_run&&!A.error;async function oe(e){if(te<=0){N(n(`giveGift.recipientRequired`));return}if(!E.trim()){N(n(`action.reasonRequired`));return}F(!0),N(``);try{let n=await k.action(`/api/actions/give-gift`,re(e));j(n),e&&!n.error&&t?.()}catch(e){N(O(e))}finally{F(!1)}}return(0,H.jsxs)(`div`,{className:`give-gift-form`,children:[(0,H.jsxs)(`div`,{className:`give-gift-summary`,children:[(0,H.jsx)(xe,{size:16}),(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`strong`,{children:e.Title||`Gift #${e.GiftID}`}),(0,H.jsxs)(`span`,{className:`mono`,children:[`#`,e.GiftID,` · ⭐ `,e.Stars]})]})]}),(0,H.jsxs)(`div`,{className:`give-gift-tabs`,role:`group`,"aria-label":n(`giveGift.recipientKind`),children:[(0,H.jsxs)(`button`,{type:`button`,className:`btn ${r===`user`?`primary`:``}`,onClick:()=>{i(`user`),j(null)},children:[(0,H.jsx)($e,{size:15}),` `,n(`giveGift.recipientUser`)]}),(0,H.jsxs)(`button`,{type:`button`,className:`btn ${r===`channel`?`primary`:``}`,onClick:()=>{i(`channel`),m(!1),j(null)},children:[(0,H.jsx)(et,{size:15}),` `,n(`giveGift.recipientChannel`)]})]}),r===`user`?(0,H.jsx)(On,{label:n(`giveGift.pickUser`),value:a,onChange:e=>{o(e),j(null)}}):(0,H.jsx)(An,{label:n(`giveGift.pickChannel`),value:s,onChange:e=>{c(e),j(null)}}),(0,H.jsxs)(`label`,{className:`form-field`,children:[(0,H.jsx)(`span`,{children:n(`giveGift.sender`)}),(0,H.jsx)(`input`,{value:br,disabled:!0,readOnly:!0}),(0,H.jsx)(`small`,{className:`field-hint`,children:n(`giveGift.senderHint`)})]}),(0,H.jsxs)(`label`,{className:`form-field`,children:[(0,H.jsx)(`span`,{children:n(`giveGift.message`)}),(0,H.jsx)(`textarea`,{value:l,rows:2,maxLength:128,onChange:e=>{u(e.target.value),j(null)},placeholder:n(`giveGift.messagePlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:d,onChange:e=>{f(e.target.checked),j(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:n(`giveGift.hideName`)})]}),r===`user`&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`gift-switch`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:p,onChange:e=>{m(e.target.checked),e.target.checked||(x(`0`),C(`0`),T(`0`)),j(null)}}),(0,H.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,H.jsx)(`span`,{})}),(0,H.jsx)(`span`,{children:n(`giveGift.upgrade`)})]}),p&&(0,H.jsx)(`p`,{className:`give-gift-upgrade-note`,children:n(`giveGift.upgradeNote`)}),p&&v&&(0,H.jsx)(J,{children:v}),p&&h&&(0,H.jsxs)(`div`,{className:`gift-fields-grid give-gift-attrs`,children:[(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:n(`giveGift.model`)}),(0,H.jsxs)(`select`,{value:b,onChange:e=>{x(e.target.value),j(null)},children:[(0,H.jsx)(`option`,{value:`0`,children:n(`giveGift.random`)}),(h.models??[]).map(e=>(0,H.jsx)(`option`,{value:e.id,children:xr(e)},e.id))]})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:n(`giveGift.pattern`)}),(0,H.jsxs)(`select`,{value:S,onChange:e=>{C(e.target.value),j(null)},children:[(0,H.jsx)(`option`,{value:`0`,children:n(`giveGift.random`)}),(h.patterns??[]).map(e=>(0,H.jsx)(`option`,{value:e.id,children:xr(e)},e.id))]})]}),(0,H.jsxs)(`label`,{children:[(0,H.jsx)(`span`,{children:n(`giveGift.backdrop`)}),(0,H.jsxs)(`select`,{value:w,onChange:e=>{T(e.target.value),j(null)},children:[(0,H.jsx)(`option`,{value:`0`,children:n(`giveGift.random`)}),(h.backdrops??[]).map(e=>(0,H.jsx)(`option`,{value:e.id,children:xr(e)},e.id))]})]})]})]}),(0,H.jsxs)(`label`,{className:`form-field`,children:[(0,H.jsx)(`span`,{children:n(`action.reason`)}),(0,H.jsx)(`textarea`,{value:E,rows:2,onChange:e=>D(e.target.value),placeholder:n(`action.reasonPlaceholder`)})]}),(0,H.jsxs)(`div`,{className:`command-preview`,children:[(0,H.jsx)(`div`,{className:`preview-head`,children:n(`action.requestPreview`)}),(0,H.jsx)(Pt,{value:JSON.stringify(ie,null,2)})]}),M&&(0,H.jsx)(J,{children:M}),A&&(0,H.jsxs)(`div`,{className:`result-box`,children:[(0,H.jsxs)(`div`,{className:`result-title`,children:[A.error?(0,H.jsx)(ee,{size:16}):(0,H.jsx)(I,{size:16}),(0,H.jsx)(`strong`,{children:A.message||A.error||n(`action.result`)})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:n(`action.commandID`)}),(0,H.jsx)(`strong`,{children:A.command_id})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:n(`action.status`)}),(0,H.jsx)(`strong`,{children:A.status})]}),(0,H.jsxs)(`div`,{className:`result-line`,children:[(0,H.jsx)(`span`,{children:n(`action.dryRun`)}),(0,H.jsx)(`strong`,{children:A.dry_run?n(`common.yes`):n(`common.no`)})]}),A.details&&(0,H.jsx)(Pt,{value:JSON.stringify(A.details,null,2)})]}),(0,H.jsxs)(`div`,{className:`give-gift-form-actions`,children:[(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>oe(!1),disabled:P,children:[P?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(je,{size:15}),n(A?`action.runAgain`:`action.runDry`)]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`button`,onClick:()=>oe(!0),disabled:P||!ae,children:[(0,H.jsx)(xe,{size:15}),n(`giveGift.confirm`)]})]})]})}function Cr(){let{t:e}=U(),[t,n]=(0,g.useState)([]),[r,i]=(0,g.useState)(``),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(!1);async function d(){u(!0),c(``);try{let e=(await k.gifts()).Gifts??[];n(e),o(t=>t??e[0]??null)}catch(e){c(O(e))}finally{u(!1)}}(0,g.useEffect)(()=>{d()},[]);let f=(0,g.useMemo)(()=>{let e=r.trim().toLowerCase();return e?t.filter(t=>String(t.GiftID).includes(e)||t.Title.toLowerCase().includes(e)):t},[t,r]);return(0,H.jsxs)(K,{title:e(`giveGifts.pageTitle`),eyebrow:e(`giveGifts.eyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>d(),disabled:l,children:[(0,H.jsx)(Fe,{size:15}),` `,e(`common.refresh`)]}),children:[s&&(0,H.jsx)(J,{children:s}),(0,H.jsx)(`p`,{className:`give-gift-upgrade-note`,children:e(`giveGifts.hint`)}),(0,H.jsxs)(`div`,{className:`give-gift-layout`,children:[(0,H.jsxs)(`section`,{className:`give-gift-picker`,children:[(0,H.jsxs)(`div`,{className:`give-gift-picker-head`,children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`giveGifts.searchPlaceholder`)})]}),(0,H.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:f.length,total:t.length})})]}),(0,H.jsxs)(`div`,{className:`give-gift-picker-list`,role:`listbox`,"aria-label":e(`giveGifts.pickGift`),children:[f.map(t=>{let n=a?.GiftID===t.GiftID;return(0,H.jsxs)(`button`,{type:`button`,role:`option`,"aria-selected":n,className:`give-gift-option ${n?`selected`:``} ${t.Enabled?``:`gift-row-disabled`}`,onClick:()=>o(t),children:[(0,H.jsx)(Un,{className:`give-gift-thumb`,cacheKey:`${t.GiftID}:${t.Revision}`,loader:()=>k.giftAnimation(t.GiftID)}),(0,H.jsxs)(`span`,{className:`give-gift-option-info`,children:[(0,H.jsx)(`strong`,{children:t.Title||`Gift #${t.GiftID}`}),(0,H.jsxs)(`span`,{className:`mono`,children:[`#`,t.GiftID]})]}),(0,H.jsx)(`span`,{className:`give-gift-option-price`,children:t.Enabled?(0,H.jsxs)(Y,{children:[`⭐ `,t.Stars]}):(0,H.jsx)(Y,{tone:`neutral`,children:e(`common.disabled`)})})]},t.GiftID)}),f.length===0&&!l&&(0,H.jsx)(`div`,{className:`official-gift-empty`,children:e(`common.noResults`)})]})]}),(0,H.jsx)(`section`,{className:`give-gift-panel`,children:a?(0,H.jsx)(Sr,{gift:a,onDone:()=>void d()},a.GiftID):(0,H.jsxs)(`div`,{className:`give-gift-empty-panel`,children:[(0,H.jsx)(xe,{size:26}),(0,H.jsx)(`p`,{children:e(`giveGifts.selectPrompt`)})]})})]})]})}var wr=`open,in_review,action_pending,action_failed,appeal_review`,Tr=[{value:wr,labelKey:`moderation.statusFilter.active`},{value:`open,in_review,action_pending,action_failed,resolved,dismissed,appeal_review`,labelKey:`moderation.statusFilter.all`},{value:`open`,labelKey:`moderation.status.open`},{value:`in_review`,labelKey:`moderation.status.in_review`},{value:`action_pending`,labelKey:`moderation.status.action_pending`},{value:`action_failed`,labelKey:`moderation.status.action_failed`},{value:`appeal_review`,labelKey:`moderation.status.appeal_review`},{value:`resolved`,labelKey:`moderation.status.resolved`},{value:`dismissed`,labelKey:`moderation.status.dismissed`}];function Er({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(wr),[i,a]=(0,g.useState)(``),[o,s]=(0,g.useState)([]),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(``);async function f(){l(!0),d(``);try{let e=new URLSearchParams({statuses:n,limit:`100`});i.trim()&&e.set(`assigned_to`,i.trim()),s((await k.moderationCases(e)).cases)}catch(e){d(O(e))}finally{l(!1)}}(0,g.useEffect)(()=>{f()},[]);let p=o.filter(e=>e.Status===`action_pending`||e.Status===`action_failed`).length,m=o.filter(e=>e.Severity===4).length;return(0,H.jsxs)(K,{title:t(`route.moderation`),eyebrow:t(`moderation.casesEyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:f,disabled:c,children:[(0,H.jsx)(Fe,{size:15,className:c?`spin`:``}),` `,t(`common.refresh`)]}),children:[u&&(0,H.jsx)(J,{children:u}),(0,H.jsxs)(`div`,{className:`metric-row`,children:[(0,H.jsx)(X,{label:t(`moderation.currentQueue`),value:String(o.length)}),(0,H.jsx)(X,{label:t(`moderation.criticalCases`),value:String(m),tone:m?`danger`:`neutral`}),(0,H.jsx)(X,{label:t(`moderation.pendingOrFailed`),value:String(p),tone:p?`warn`:`good`})]}),(0,H.jsx)(Ot,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),f()},children:[(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.status`)}),(0,H.jsx)(`select`,{"aria-label":t(`moderation.statusFilter`),value:n,onChange:e=>r(e.target.value),children:Tr.map(e=>(0,H.jsx)(`option`,{value:e.value,children:t(e.labelKey)},e.value))})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`moderation.assignee`)}),(0,H.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`moderation.allAssignees`)})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:c,children:[(0,H.jsx)(Be,{size:15}),` `,t(`common.search`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`moderation.case`)}),(0,H.jsx)(`th`,{children:t(`moderation.target`)}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`moderation.severity`)}),(0,H.jsx)(`th`,{children:t(`moderation.reportsAndReporters`)}),(0,H.jsx)(`th`,{children:t(`moderation.assignee`)}),(0,H.jsx)(`th`,{children:t(`moderation.latestReport`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[o.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsxs)(`td`,{className:`mono`,children:[`#`,n.ID]}),(0,H.jsx)(`td`,{className:`mono`,children:Ar(t,n.Target.Type,n.Target.ID)}),(0,H.jsx)(`td`,{children:(0,H.jsx)(Dr,{status:n.Status})}),(0,H.jsx)(`td`,{children:(0,H.jsx)(Or,{value:n.Severity})}),(0,H.jsxs)(`td`,{children:[n.ReportCount,` / `,n.DistinctReporterCount]}),(0,H.jsx)(`td`,{children:n.AssignedTo||`-`}),(0,H.jsx)(`td`,{children:G(n.LastReportAt)}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/moderation/${n.ID}`),children:[t(`moderation.review`),` `,(0,H.jsx)(z,{size:14})]})})]},n.ID)),o.length===0&&(0,H.jsx)(Mt,{colSpan:8})]})]})})]})}function Dr({status:e}){let{t}=U();return(0,H.jsx)(Y,{tone:e===`resolved`||e===`dismissed`?`good`:e===`action_failed`?`danger`:e===`action_pending`?`warn`:`neutral`,children:kr(t,`status`,e)})}function Or({value:e}){let{t}=U(),n=[``,`low`,`medium`,`high`,`critical`][e];return(0,H.jsx)(Y,{tone:e>=4?`danger`:e>=3?`warn`:`neutral`,children:n?t(`moderation.severity.${n}`):e})}function kr(e,t,n){let r=`moderation.${t}.${n}`,i=e(r);return i===r?n:i}function Ar(e,t,n){return`${kr(e,`targetType`,t)} #${n}`}function jr({id:e,navigate:t}){let{t:n}=U(),[r,i]=(0,g.useState)(null),[a,o]=(0,g.useState)(null),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(`no_violation`),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(``),[h,_]=(0,g.useState)(!0),[v,y]=(0,g.useState)(!1),[b,x]=(0,g.useState)(``);function S(e){o(e),e&&(f(e.Items.filter(e=>e.Kind===`message`).map(e=>Number(e.ItemID)).filter(e=>Number.isSafeInteger(e)&&e>0).join(`, `)),m(String(e.ReporterUserID)))}async function C(){x(``);try{let t=await k.moderationCase(e);i(t);let n=t.ReportIDs[0];S(n?await k.moderationReport(n):null)}catch(e){x(O(e))}}(0,g.useEffect)(()=>{C()},[e]);let w=(0,g.useMemo)(()=>Mr(l,r?.Case.Target.Type,Nr(d),Number(p),h),[l,r?.Case.Target.Type,d,p,h]),T=(0,g.useMemo)(()=>r?Pr(r,n):{actions:[],label:n(`common.none`),blocked:!1},[r,n]);async function E(){if(r){y(!0),x(``);try{await k.claimModerationCase(e,r.Case.Version),await C()}catch(e){x(O(e))}finally{y(!1)}}}async function D(){if(!r||!s.trim()){x(n(`moderation.reasonRequired`));return}if(l===`delete_messages`&&w.length===0){x(r.Case.Target.Type===`user`?n(`moderation.privateDeleteValidation`):n(`moderation.channelDeleteValidation`));return}if(window.confirm(n(`moderation.confirmDecision`,{decision:Fr(n,l)}))){y(!0),x(``);try{i((await k.decideModerationCase(e,{expected_version:r.Case.Version,reason:s.trim(),kind:l===`no_violation`?`no_violation`:`violation`,actions:w})).case),c(``)}catch(e){x(O(e))}finally{y(!1)}}}async function A(t,a){if(!r||!s.trim()){x(n(`moderation.appealReasonRequired`));return}if(window.confirm(n(a?`moderation.confirmGrantAppeal`:`moderation.confirmDenyAppeal`))){y(!0);try{i((await k.reviewModerationAppeal(e,t,{expected_version:r.Case.Version,reason:s.trim(),granted:a,actions:a?T.actions:[]})).case),c(``)}catch(e){x(O(e))}finally{y(!1)}}}if(b&&!r)return(0,H.jsx)(J,{children:b});if(!r)return(0,H.jsx)(Nt,{label:n(`moderation.loadingCase`)});let j=r.Case,M=j.Status===`open`||j.Status===`in_review`||j.Status===`appeal_review`,N=(j.Status===`in_review`||j.Status===`action_failed`)&&!!j.AssignedTo,P=N&&(j.Status!==`action_failed`||l!==`no_violation`),F=r.Appeals.find(e=>e.Status===`pending`);return(0,H.jsxs)(K,{title:n(`moderation.caseDetailTitle`,{id:j.ID}),eyebrow:n(`moderation.caseDetailEyebrow`),actions:(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/moderation`),children:[(0,H.jsx)(ae,{size:15}),` `,n(`moderation.backToQueue`)]}),(0,H.jsxs)(`button`,{className:`btn icon-text`,onClick:C,children:[(0,H.jsx)(Fe,{size:15}),` `,n(`common.refresh`)]})]}),children:[b&&(0,H.jsx)(J,{children:b}),(0,H.jsx)(kt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:Ar(n,j.Target.Type,j.Target.ID)}),(0,H.jsx)(`div`,{className:`entity-subtitle`,children:n(`moderation.versionAndUpdated`,{version:j.Version,time:G(j.UpdatedAt)})})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(Dr,{status:j.Status}),(0,H.jsx)(Or,{value:j.Severity})]})]}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`moderation.target`),value:Ar(n,j.Target.Type,j.Target.ID),mono:!0}),(0,H.jsx)(Z,{label:n(`moderation.reportCount`),value:n(`moderation.reportCountValue`,{reports:j.ReportCount,reporters:j.DistinctReporterCount})}),(0,H.jsx)(Z,{label:n(`moderation.assignee`),value:j.AssignedTo||`-`}),(0,H.jsx)(Z,{label:n(`moderation.firstAndLatestReport`),value:`${G(j.FirstReportAt)} / ${G(j.LastReportAt)}`})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`moderation.evidence`),text:n(`moderation.evidenceHint`)}),(0,H.jsx)(`div`,{className:`toolbar`,children:r.ReportIDs.map(e=>(0,H.jsxs)(`button`,{className:`btn`,onClick:async()=>S(await k.moderationReport(e)),children:[`#`,e]},e))}),a&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`moderation.sourceAndReason`),value:`${kr(n,`source`,a.Source)} / ${kr(n,`reason`,a.Reason)}`}),(0,H.jsx)(Z,{label:n(`moderation.reporter`),value:String(a.ReporterUserID),mono:!0}),(0,H.jsx)(Z,{label:n(`moderation.option`),value:a.Option,mono:!0}),(0,H.jsx)(Z,{label:n(`common.time`),value:G(a.CreatedAt)})]}),a.Comment&&(0,H.jsx)(`p`,{className:`about-text`,children:a.Comment}),(0,H.jsx)(Pt,{value:JSON.stringify(a,null,2)})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`moderation.decisionAudit`),text:n(`moderation.decisionAuditHint`)}),(0,H.jsx)(Pt,{value:JSON.stringify({decisions:r.Decisions,actions:r.Actions},null,2)})]}),r.Appeals.length>0&&(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`moderation.appeals`)}),(0,H.jsx)(Pt,{value:JSON.stringify(r.Appeals,null,2)})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`moderation.caseActions`)}),M&&(0,H.jsxs)(`button`,{className:`btn primary icon-text`,disabled:v,onClick:E,children:[(0,H.jsx)(Ve,{size:15}),` `,j.AssignedTo?n(`moderation.renewClaim`):n(`moderation.claimCase`)]}),(0,H.jsxs)(`label`,{className:`field`,children:[(0,H.jsx)(`span`,{children:n(`moderation.reviewReason`)}),(0,H.jsx)(`textarea`,{value:s,onChange:e=>c(e.target.value),rows:5})]}),(0,H.jsxs)(`label`,{className:`field`,children:[(0,H.jsx)(`span`,{children:n(`moderation.decisionPreset`)}),(0,H.jsxs)(`select`,{value:l,onChange:e=>u(e.target.value),children:[(0,H.jsx)(`option`,{value:`no_violation`,children:n(`moderation.preset.noViolation`)}),(0,H.jsx)(`option`,{value:`scam`,children:n(`moderation.preset.scam`)}),(0,H.jsx)(`option`,{value:`fake`,children:n(`moderation.preset.fake`)}),(0,H.jsx)(`option`,{value:`freeze`,children:n(`moderation.preset.freeze`)}),(0,H.jsx)(`option`,{value:`scam_freeze`,children:n(`moderation.preset.scamFreeze`)}),(0,H.jsx)(`option`,{value:`fake_freeze`,children:n(`moderation.preset.fakeFreeze`)}),(0,H.jsx)(`option`,{value:`delete_messages`,children:n(`moderation.preset.deleteMessages`)}),(0,H.jsx)(`option`,{value:`delete_account`,children:n(`moderation.preset.deleteAccount`)})]})]}),l===`delete_messages`&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`field`,children:[(0,H.jsx)(`span`,{children:n(`moderation.evidenceMessageIDs`)}),(0,H.jsx)(`input`,{value:d,onChange:e=>f(e.target.value),placeholder:`101, 102`})]}),j.Target.Type===`user`&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`field`,children:[(0,H.jsx)(`span`,{children:n(`moderation.privateOwnerUserID`)}),(0,H.jsx)(`input`,{value:p,onChange:e=>m(e.target.value),inputMode:`numeric`})]}),(0,H.jsxs)(`label`,{className:`field checkbox-field`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:h,onChange:e=>_(e.target.checked)}),(0,H.jsx)(`span`,{children:n(`moderation.revokeForBoth`)})]})]}),(0,H.jsx)(J,{children:n(`moderation.evidenceValidationHint`)})]}),j.Status===`action_failed`&&l===`no_violation`&&(0,H.jsx)(J,{children:n(`moderation.failedActionHint`)}),N&&(0,H.jsxs)(`button`,{className:`btn danger icon-text`,disabled:v||!P,onClick:D,children:[(0,H.jsx)(I,{size:15}),` `,j.Status===`action_failed`?n(`moderation.retryAction`):n(`moderation.submitDecision`)]}),F&&j.AssignedTo&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`moderation.appealReviewTitle`,{id:F.ID})}),(0,H.jsx)(Z,{label:n(`moderation.automaticRemedy`),value:T.label}),T.blocked&&(0,H.jsx)(J,{children:n(`moderation.irreversibleAppealHint`)}),(0,H.jsx)(`button`,{className:`btn`,disabled:v,onClick:()=>A(F.ID,!1),children:n(`moderation.denyAppeal`)}),(0,H.jsx)(`button`,{className:`btn primary`,disabled:v||T.blocked,onClick:()=>A(F.ID,!0),children:n(`moderation.grantAppeal`)})]})]})})]})}function Mr(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 Nr(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 Pr(e,t){let n=!1,r=!1,i=!1;for(let t of[...e.Actions].sort((e,t)=>e.ID-t.ID))if(t.Status===`succeeded`)switch(t.Kind){case`mark_scam`:case`mark_fake`:n=!0;break;case`clear_peer_flags`:n=!1;break;case`freeze_account`:r=!0;break;case`unfreeze_account`:r=!1;break;case`delete_private_message`:case`delete_channel_message`:case`delete_account`:i=!0;break}let a=[],o=[];return n&&(a.push({kind:`clear_peer_flags`,payload:{}}),o.push(t(`moderation.remedy.clearFlags`))),r&&(a.push({kind:`unfreeze_account`,payload:{}}),o.push(t(`moderation.remedy.unfreeze`))),{actions:a,label:o.join(` + `)||t(`moderation.remedy.none`),blocked:i}}function Fr(e,t){return e(`moderation.preset.${{no_violation:`noViolation`,scam:`scam`,fake:`fake`,freeze:`freeze`,scam_freeze:`scamFreeze`,fake_freeze:`fakeFreeze`,delete_messages:`deleteMessages`,delete_account:`deleteAccount`}[t]}`)}var Ir=[`pending`,`approved`,`rejected`,`revoked`],Lr=[`user`,`channel`];function Rr({navigate:e}){let{t}=U(),{can:n}=Vt(),r=n(Rt),i=n(It),[a,o]=(0,g.useState)(`requests`),[s,c]=(0,g.useState)([]),[l,u]=(0,g.useState)([]),[d,f]=(0,g.useState)(``),[p,m]=(0,g.useState)(!1);async function h(){f(``),m(!1);try{let[e,t]=await Promise.all([k.botVerifiers(new URLSearchParams({limit:`200`})),k.verificationIcons(new URLSearchParams({limit:`200`}))]);c(e.rows??[]),u(t.rows??[])}catch(e){if(e instanceof v&&e.status===403){c([]),u([]),m(!0);return}f(O(e))}}(0,g.useEffect)(()=>{h()},[]);let _=[{key:`requests`,label:t(`botverification.tabRequests`),icon:(0,H.jsx)(Ge,{size:15})},{key:`verifiers`,label:t(`botverification.tabVerifiers`),icon:(0,H.jsx)(le,{size:15})},{key:`icons`,label:t(`botverification.tabIcons`),icon:(0,H.jsx)(qe,{size:15})},{key:`marks`,label:t(`botverification.tabMarks`),icon:(0,H.jsx)(F,{size:15})}];return(0,H.jsxs)(K,{title:t(`botverification.pageTitle`),eyebrow:t(`botverification.eyebrow`),actions:i?(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>e(`/verification`),children:[(0,H.jsx)(ge,{size:15}),` `,t(`botverification.openOfficial`)]}):void 0,children:[d&&(0,H.jsx)(J,{children:d}),p&&(0,H.jsx)(J,{children:t(`botverification.rosterDenied`)}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:t(`botverification.explainTitle`),text:t(`botverification.explainText`)}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:t(`botverification.explainIcon`)}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:t(`botverification.explainOfficial`)}),!r&&(0,H.jsx)(`p`,{className:`bot-create-note`,children:t(`botverification.manageMissing`)})]}),(0,H.jsx)(`div`,{className:`toolbar`,role:`group`,"aria-label":t(`botverification.pageTitle`),children:_.map(e=>(0,H.jsxs)(`button`,{className:`btn icon-text ${a===e.key?`primary`:``}`,type:`button`,"aria-pressed":a===e.key,onClick:()=>o(e.key),children:[e.icon,` `,e.label]},e.key))}),a===`requests`&&(0,H.jsx)(zr,{navigate:e,verifiers:s}),a===`verifiers`&&(0,H.jsx)(Br,{verifiers:s,icons:l,canManage:r,onChanged:h,navigate:e}),a===`icons`&&(0,H.jsx)(Vr,{icons:l,verifiers:s,canManage:r,onChanged:h}),a===`marks`&&(0,H.jsx)(Hr,{verifiers:s,canManage:r,navigate:e})]})}function zr({navigate:e,verifiers:t}){let{t:n}=U(),[r,i]=(0,g.useState)(`pending`),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(`all`),[l,u]=(0,g.useState)(``),[d,f]=(0,g.useState)(`50`),[p,m]=(0,g.useState)([]),[h,_]=(0,g.useState)({}),[v,y]=(0,g.useState)(!1),[b,x]=(0,g.useState)(``),[S,C]=(0,g.useState)(!1),[w,T]=(0,g.useState)(``);async function E(e=!1){C(!0),T(``);let t=new URLSearchParams({limit:d});r!==`all`&&t.set(`status`,r),a&&t.set(`verifier_bot_id`,a),s!==`all`&&t.set(`peer_type`,s),l.trim()&&t.set(`q`,l.trim().replace(/^@/,``)),e&&b&&t.set(`before_id`,b);try{let n=await k.customVerificationRequests(t),r=n.rows??[];m(t=>e?[...t,...r]:r),x(n.next_before_id??``),y(!!n.has_more)}catch(e){T(O(e))}finally{C(!1)}}async function D(){try{_((await k.botVerificationCounts()).counts??{})}catch(e){T(O(e))}}(0,g.useEffect)(()=>{E(!1),D()},[]);function A(){E(!1),D()}return(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`botverification.queueTitle`),text:n(`botverification.queueHint`),action:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:A,disabled:S,children:[(0,H.jsx)(Fe,{size:15,className:S?`spin`:``}),` `,n(`common.refresh`)]})}),w&&(0,H.jsx)(J,{children:w}),(0,H.jsx)(`div`,{className:`metric-row`,children:Ir.map(e=>(0,H.jsx)(X,{label:n(`botverification.status.${e}`),value:h[e]??`0`,mono:!0,tone:Kr(e,h[e]??`0`)},e))})]}),(0,H.jsx)(Ot,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),E(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:l,onChange:e=>u(e.target.value),placeholder:n(`botverification.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:n(`common.status`)}),(0,H.jsxs)(`select`,{value:r,onChange:e=>i(e.target.value),children:[(0,H.jsx)(`option`,{value:`all`,children:n(`botverification.statusAll`)}),Ir.map(e=>(0,H.jsx)(`option`,{value:e,children:n(`botverification.status.${e}`)},e))]})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:n(`botverification.verifier`)}),(0,H.jsx)(Ur,{value:a,verifiers:t,onChange:o})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:n(`botverification.peerType`)}),(0,H.jsxs)(`select`,{value:s,onChange:e=>c(e.target.value),children:[(0,H.jsx)(`option`,{value:`all`,children:n(`botverification.peerTypeAll`)}),Lr.map(e=>(0,H.jsx)(`option`,{value:e,children:n(`botverification.peer.${e}`)},e))]})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:n(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:S,children:[S?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(Ie,{size:15}),` `,n(`common.search`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:n(`common.id`)}),(0,H.jsx)(`th`,{children:n(`botverification.verifier`)}),(0,H.jsx)(`th`,{children:n(`botverification.target`)}),(0,H.jsx)(`th`,{children:n(`botverification.applicant`)}),(0,H.jsx)(`th`,{children:n(`botverification.reason`)}),(0,H.jsx)(`th`,{children:n(`common.status`)}),(0,H.jsx)(`th`,{children:n(`botverification.createdAt`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[p.map(t=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:(0,H.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/bot-verification/${t.ID}`),children:[`#`,t.ID]})}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`strong`,{children:W(t.VerifierBotUsername)||t.VerifierBotID}),(0,H.jsx)(`div`,{className:`entity-subtitle mono`,children:t.VerifierBotID})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`strong`,{children:qr(t)}),(0,H.jsxs)(`div`,{className:`entity-subtitle mono`,children:[n(`botverification.peer.${t.PeerType}`),` · `,t.PeerID]})]}),(0,H.jsxs)(`td`,{children:[W(t.ApplicantUsername)||`-`,(0,H.jsx)(`div`,{className:`entity-subtitle mono`,children:t.ApplicantUserID})]}),(0,H.jsx)(`td`,{className:`truncate`,children:t.Reason||`-`}),(0,H.jsx)(`td`,{children:(0,H.jsx)(Wr,{status:t.Status})}),(0,H.jsx)(`td`,{children:G(t.CreatedAt)||`-`}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/bot-verification/${t.ID}`),children:[(0,H.jsx)(Ge,{size:14}),` `,n(`common.detail`),` `,(0,H.jsx)(z,{size:14})]})})]},t.ID)),p.length===0&&(0,H.jsx)(Mt,{colSpan:8})]})]})}),v&&(0,H.jsx)(`div`,{className:`toolbar`,children:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>E(!0),disabled:S,children:[S?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(fe,{size:15}),` `,n(`common.loadMore`)]})})]})}function Br({verifiers:e,icons:t,canManage:n,onChanged:r,navigate:i}){let{t:a}=U(),[o,s]=(0,g.useState)(null),[c,l]=(0,g.useState)(null),[u,d]=(0,g.useState)(``),[f,p]=(0,g.useState)(``),[m,h]=(0,g.useState)(``),[_,v]=(0,g.useState)(!1),y=t.filter(e=>e.Active),b=y.map(e=>({value:e.DocumentID,label:`${e.Name} · ${e.DocumentID}`}));if(u&&!b.some(e=>e.value===u)){let e=t.find(e=>e.DocumentID===u);b.unshift({value:u,label:`${e?.Name??u} · ${u} (${a(`botverification.iconInactive`)})`})}function x(e){l(e),s(null),d(e.IconDocumentID),p(e.CompanyName),h(e.DefaultDescription),v(e.CanModifyCustomDescription)}function S(){l(null),s(null),d(``),p(``),h(``),v(!1)}function C(){return{bot_id:c?c.BotID:o?String(o.ID):`0`,icon_document_id:u||`0`,company_name:f.trim(),default_description:m.trim(),can_modify_custom_description:_,version:c?c.Version:`0`}}return(0,H.jsxs)(H.Fragment,{children:[n&&(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:a(c?`botverification.updateTitle`:`botverification.grantTitle`),text:a(`botverification.grantHint`),action:c?(0,H.jsx)(`button`,{className:`btn icon-text`,type:`button`,onClick:S,children:a(`botverification.cancelEdit`)}):void 0}),c?(0,H.jsx)(`p`,{className:`bot-create-note`,children:a(`botverification.editing`,{bot:W(c.BotUsername)||c.BotID,version:c.Version})}):(0,H.jsx)(kn,{label:a(`botverification.grantBot`),value:o,onChange:s}),(0,H.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:a(`botverification.grantIcon`)}),(0,H.jsxs)(`select`,{value:u,onChange:e=>d(e.target.value),children:[(0,H.jsx)(`option`,{value:``,children:a(`botverification.grantIconPick`)}),b.map(e=>(0,H.jsx)(`option`,{value:e.value,children:e.label},e.value))]})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:a(`botverification.company`)}),(0,H.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:a(`botverification.companyPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:a(`botverification.defaultDescription`)}),(0,H.jsx)(`input`,{value:m,onChange:e=>h(e.target.value),placeholder:a(`botverification.defaultDescriptionPlaceholder`)})]})]}),(0,H.jsxs)(`label`,{className:`checkline`,children:[(0,H.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),a(`botverification.canModify`)]}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:a(`botverification.canModifyHint`)}),y.length===0&&(0,H.jsx)(J,{children:a(`botverification.noActiveIcons`)}),(0,H.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,H.jsx)(`span`,{className:`bot-create-note`,children:a(`botverification.grantNote`)}),(0,H.jsx)(Q,{label:a(c?`botverification.update`:`botverification.grant`),icon:(0,H.jsx)(Me,{size:15}),tone:`neutral`,path:`/api/actions/grant-bot-verifier`,payload:C,onDone:()=>{S(),r()}})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:a(`botverification.verifiersTitle`),text:a(`botverification.verifiersHint`),action:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:r,children:[(0,H.jsx)(Fe,{size:15}),` `,a(`common.refresh`)]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:a(`botverification.bot`)}),(0,H.jsx)(`th`,{children:a(`botverification.company`)}),(0,H.jsx)(`th`,{children:a(`botverification.icon`)}),(0,H.jsx)(`th`,{children:a(`botverification.canModifyShort`)}),(0,H.jsx)(`th`,{children:a(`common.status`)}),(0,H.jsx)(`th`,{children:a(`botverification.markCount`)}),(0,H.jsx)(`th`,{children:a(`botverification.grantedBy`)}),(0,H.jsx)(`th`,{children:a(`common.updatedAt`)}),n&&(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[e.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`button`,{className:`row-link`,type:`button`,onClick:()=>i(`/bots/${e.BotID}`),children:(0,H.jsx)(`strong`,{children:W(e.BotUsername)||e.BotName||e.BotID})}),(0,H.jsx)(`div`,{className:`entity-subtitle mono`,children:e.BotID})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`strong`,{children:e.CompanyName||`-`}),(0,H.jsx)(`div`,{className:`entity-subtitle truncate`,children:e.DefaultDescription||a(`botverification.notProvided`)})]}),(0,H.jsxs)(`td`,{children:[e.IconName||`-`,(0,H.jsx)(`div`,{className:`entity-subtitle mono`,children:e.IconDocumentID})]}),(0,H.jsx)(`td`,{children:e.CanModifyCustomDescription?a(`common.yes`):a(`common.no`)}),(0,H.jsx)(`td`,{children:e.Enabled?(0,H.jsx)(Y,{tone:`good`,children:a(`botverification.enabled`)}):(0,H.jsx)(Y,{tone:`warn`,children:a(`botverification.disabled`)})}),(0,H.jsx)(`td`,{className:`mono`,children:String(e.MarkCount??`0`)}),(0,H.jsxs)(`td`,{children:[e.GrantedBy||`-`,(0,H.jsx)(`div`,{className:`entity-subtitle truncate`,children:e.GrantReason||`-`})]}),(0,H.jsx)(`td`,{children:G(e.UpdatedAt)||`-`}),n&&(0,H.jsx)(`td`,{children:(0,H.jsxs)(`div`,{className:`row-actions`,children:[(0,H.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>x(e),children:a(`botverification.edit`)}),(0,H.jsx)(Q,{label:e.Enabled?a(`botverification.disable`):a(`botverification.enable`),icon:e.Enabled?(0,H.jsx)(Ne,{size:14}):(0,H.jsx)(Pe,{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,H.jsx)(Q,{label:a(`botverification.revokeVerifier`),icon:(0,H.jsx)(Ye,{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,H.jsx)(Mt,{colSpan:n?9:8})]})]})}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:a(`botverification.disableHint`)}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:a(`botverification.revokeVerifierHint`)})]})]})}function Vr({icons:e,verifiers:t,canManage:n,onChanged:r}){let{t:i}=U(),[a,o]=(0,g.useState)(``),[s,c]=(0,g.useState)(``),[l,u]=(0,g.useState)(``);function d(){let e={document_id:a.trim()||`0`,name:s.trim()};return l&&(e.owner_bot_id=l),e}return(0,H.jsxs)(H.Fragment,{children:[n&&(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:i(`botverification.addIconTitle`),text:i(`botverification.addIconHint`)}),(0,H.jsxs)(`div`,{className:`bot-create-fields`,children:[(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:i(`botverification.iconDocument`)}),(0,H.jsx)(`input`,{value:a,onChange:e=>o(e.target.value),inputMode:`numeric`,placeholder:`5361371319611781774`})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:i(`botverification.iconName`)}),(0,H.jsx)(`input`,{value:s,onChange:e=>c(e.target.value),placeholder:i(`botverification.iconNamePlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:i(`botverification.iconOwner`)}),(0,H.jsxs)(`select`,{value:l,onChange:e=>u(e.target.value),children:[(0,H.jsx)(`option`,{value:``,children:i(`botverification.iconOwnerShared`)}),t.map(e=>(0,H.jsx)(`option`,{value:e.BotID,children:`${e.CompanyName||e.BotID} · ${W(e.BotUsername)||e.BotID}`},e.BotID))]})]})]}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:i(`botverification.iconDocumentHint`)}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:i(`botverification.iconOwnerHint`)}),(0,H.jsxs)(`div`,{className:`bot-create-actions`,children:[(0,H.jsx)(`span`,{className:`bot-create-note`,children:i(`botverification.addIconNote`)}),(0,H.jsx)(Q,{label:i(`botverification.addIcon`),icon:(0,H.jsx)(Me,{size:15}),tone:`neutral`,path:`/api/actions/upsert-verification-icon`,payload:d,onDone:()=>{o(``),c(``),u(``),r()}})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:i(`botverification.iconsTitle`),text:i(`botverification.iconsHint`),action:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:r,children:[(0,H.jsx)(Fe,{size:15}),` `,i(`common.refresh`)]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:i(`botverification.iconDocument`)}),(0,H.jsx)(`th`,{children:i(`botverification.iconName`)}),(0,H.jsx)(`th`,{children:i(`botverification.iconOwner`)}),(0,H.jsx)(`th`,{children:i(`common.status`)}),(0,H.jsx)(`th`,{children:i(`botverification.usedBy`)}),(0,H.jsx)(`th`,{children:i(`botverification.createdAt`)}),n&&(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[e.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:e.DocumentID}),(0,H.jsx)(`td`,{children:(0,H.jsx)(`strong`,{children:e.Name||`-`})}),(0,H.jsx)(`td`,{children:e.OwnerBotID&&e.OwnerBotID!==`0`?(0,H.jsxs)(H.Fragment,{children:[W(e.OwnerBotUsername)||e.OwnerBotID,(0,H.jsx)(`div`,{className:`entity-subtitle mono`,children:e.OwnerBotID})]}):(0,H.jsx)(Y,{children:i(`botverification.iconOwnerShared`)})}),(0,H.jsx)(`td`,{children:e.Active?(0,H.jsx)(Y,{tone:`good`,children:i(`botverification.iconActive`)}):(0,H.jsx)(Y,{tone:`warn`,children:i(`botverification.iconInactive`)})}),(0,H.jsx)(`td`,{className:`mono`,children:String(e.UsedByVerifiers??`0`)}),(0,H.jsx)(`td`,{children:G(e.CreatedAt)||`-`}),n&&(0,H.jsx)(`td`,{children:(0,H.jsx)(`div`,{className:`row-actions`,children:(0,H.jsx)(Q,{label:e.Active?i(`botverification.deactivateIcon`):i(`botverification.activateIcon`),icon:e.Active?(0,H.jsx)(Ne,{size:14}):(0,H.jsx)(Pe,{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,H.jsx)(Mt,{colSpan:n?7:6})]})]})}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:i(`botverification.deactivateIconHint`)})]})]})}function Hr({verifiers:e,canManage:t,navigate:n}){let{t:r}=U(),[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)(!1),[_,v]=(0,g.useState)(``),[y,b]=(0,g.useState)(!1),[x,S]=(0,g.useState)(``);async function C(e=!1){b(!0),S(``);let t=new URLSearchParams({limit:u});i&&t.set(`verifier_bot_id`,i),o!==`all`&&t.set(`peer_type`,o),c.trim()&&t.set(`q`,c.trim().replace(/^@/,``)),e&&_&&t.set(`before_id`,_);try{let n=await k.customVerifications(t),r=n.rows??[];p(t=>e?[...t,...r]:r),v(n.next_before_id??``),h(!!n.has_more)}catch(e){S(O(e))}finally{b(!1)}}return(0,g.useEffect)(()=>{C(!1)},[]),(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:r(`botverification.marksTitle`),text:r(`botverification.marksHint`),action:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>C(!1),disabled:y,children:[(0,H.jsx)(Fe,{size:15,className:y?`spin`:``}),` `,r(`common.refresh`)]})}),x&&(0,H.jsx)(J,{children:x})]}),(0,H.jsx)(Ot,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),C(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:r(`botverification.markSearchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:r(`botverification.verifier`)}),(0,H.jsx)(Ur,{value:i,verifiers:e,onChange:a})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:r(`botverification.peerType`)}),(0,H.jsxs)(`select`,{value:o,onChange:e=>s(e.target.value),children:[(0,H.jsx)(`option`,{value:`all`,children:r(`botverification.peerTypeAll`)}),Lr.map(e=>(0,H.jsx)(`option`,{value:e,children:r(`botverification.peer.${e}`)},e))]})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:r(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:y,children:[y?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(Ie,{size:15}),` `,r(`common.search`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:r(`common.id`)}),(0,H.jsx)(`th`,{children:r(`botverification.verifier`)}),(0,H.jsx)(`th`,{children:r(`botverification.target`)}),(0,H.jsx)(`th`,{children:r(`botverification.description`)}),(0,H.jsx)(`th`,{children:r(`botverification.icon`)}),(0,H.jsx)(`th`,{children:r(`botverification.createdAt`)}),t&&(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[f.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsxs)(`td`,{className:`mono`,children:[`#`,e.ID]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`strong`,{children:e.CompanyName||W(e.VerifierBotUsername)||e.VerifierBotID}),(0,H.jsx)(`div`,{className:`entity-subtitle mono`,children:W(e.VerifierBotUsername)||e.VerifierBotID})]}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`button`,{className:`row-link`,type:`button`,onClick:()=>n(Jr(e.PeerType,e.PeerID)),children:(0,H.jsx)(`strong`,{children:qr(e)})}),(0,H.jsxs)(`div`,{className:`entity-subtitle mono`,children:[r(`botverification.peer.${e.PeerType}`),` · `,e.PeerID]})]}),(0,H.jsx)(`td`,{className:`truncate`,children:e.Description||r(`botverification.notProvided`)}),(0,H.jsx)(`td`,{className:`mono`,children:e.IconDocumentID}),(0,H.jsx)(`td`,{children:G(e.CreatedAt)||`-`}),t&&(0,H.jsx)(`td`,{children:(0,H.jsx)(`div`,{className:`row-actions`,children:(0,H.jsx)(Q,{label:r(`botverification.revokeMark`),icon:(0,H.jsx)(se,{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:()=>C(!1)})})})]},e.ID)),f.length===0&&(0,H.jsx)(Mt,{colSpan:t?7:6})]})]})}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:r(`botverification.revokeMarkHint`)}),m&&(0,H.jsx)(`div`,{className:`toolbar`,children:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>C(!0),disabled:y,children:[y?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(fe,{size:15}),` `,r(`common.loadMore`)]})})]})}function Ur({value:e,verifiers:t,onChange:n}){let{t:r}=U();return(0,H.jsxs)(`select`,{value:e,onChange:e=>n(e.target.value),children:[(0,H.jsx)(`option`,{value:``,children:r(`botverification.verifierAll`)}),t.map(e=>(0,H.jsx)(`option`,{value:e.BotID,children:`${e.CompanyName||e.BotID} · ${W(e.BotUsername)||e.BotID}`+(e.Enabled?``:` (${r(`botverification.disabled`)})`)},e.BotID))]})}function Wr({status:e}){let{t}=U();return(0,H.jsx)(Y,{tone:Gr(e),children:t(`botverification.status.${e}`)})}function Gr(e){return e===`approved`?`good`:e===`pending`?`warn`:e===`rejected`?`danger`:`neutral`}function Kr(e,t){return e===`pending`?t!==`0`&&t!==``?`warn`:`neutral`:e===`approved`?`good`:`neutral`}function qr(e){return W(e.PeerUsername)||e.PeerTitle||`#${e.PeerID}`}function Jr(e,t){return e===`channel`?`/channels/${t}`:`/accounts/${t}`}function Yr({id:e,navigate:t}){let{t:n}=U(),[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.customVerificationRequest(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(),n(`botverification.conflict`)}if(d&&!r)return(0,H.jsx)(J,{children:d});if(!r)return(0,H.jsx)(Nt,{label:n(`botverification.loadingDetail`)});let _=r.request,y=Zr(r.verifier),b=r.mark_active,x=_.Status===`pending`,S=_.Status===`approved`,C=a.trim(),w=_.RequestedDescription.trim(),T=!!y?.CanModifyCustomDescription&&w!==``,E=T?w:(y?.DefaultDescription??``).trim();function D(){let e={version:_.Version};return C&&(e.internal_note=C),e}function A(){o(``),c(!1),p()}return(0,H.jsxs)(K,{title:n(`botverification.detailTitle`,{id:_.ID}),eyebrow:n(`botverification.detailEyebrow`),actions:(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/bot-verification`),children:[(0,H.jsx)(ae,{size:15}),` `,n(`common.backToList`)]}),(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:m,disabled:l,children:[(0,H.jsx)(Fe,{size:15,className:l?`spin`:``}),` `,n(`common.refresh`)]})]}),children:[d&&(0,H.jsx)(J,{children:d}),s&&(0,H.jsx)(J,{children:n(`botverification.conflict`)}),(0,H.jsx)(kt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:qr(_)}),(0,H.jsxs)(`div`,{className:`entity-subtitle mono`,children:[`#`,_.ID,` · `,n(`botverification.peer.${_.PeerType}`),`:`,_.PeerID,` · v`,_.Version]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(Wr,{status:_.Status}),b?(0,H.jsxs)(Y,{tone:`good`,children:[(0,H.jsx)(F,{size:12}),` `,n(`botverification.markActive`)]}):(0,H.jsx)(Y,{tone:`neutral`,children:n(`botverification.markInactive`)})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`botverification.explainTitle`),text:n(`botverification.explainText`)}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.explainIcon`)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`botverification.verifierSection`),text:n(`botverification.verifierHint`),action:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/bots/${_.VerifierBotID}`),children:[(0,H.jsx)(le,{size:15}),` `,n(`botverification.openVerifier`)]})}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`botverification.company`),value:y?.CompanyName||`-`}),(0,H.jsx)(Z,{label:n(`botverification.bot`),value:W(_.VerifierBotUsername)||`-`}),(0,H.jsx)(Z,{label:n(`botverification.verifierID`),value:_.VerifierBotID,mono:!0}),(0,H.jsx)(Z,{label:n(`botverification.iconDocument`),value:y?.IconDocumentID||`-`,mono:!0}),(0,H.jsx)(Z,{label:n(`botverification.iconName`),value:y?.IconName||`-`}),(0,H.jsx)(Z,{label:n(`botverification.canModifyShort`),value:y?.CanModifyCustomDescription?n(`common.yes`):n(`common.no`)})]}),(0,H.jsx)(Xr,{label:n(`botverification.defaultDescription`),children:y?.DefaultDescription?(0,H.jsx)(`p`,{className:`about-text`,children:y.DefaultDescription}):(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.notProvided`)})}),!y&&(0,H.jsx)(J,{children:n(`botverification.verifierMissing`)}),y&&!y.Enabled&&(0,H.jsx)(J,{children:n(`botverification.verifierDisabledHint`)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`botverification.targetSection`),text:n(`botverification.targetHint`),action:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(Jr(_.PeerType,_.PeerID)),children:[(0,H.jsx)(ge,{size:15}),` `,n(`botverification.openTarget`)]})}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`common.type`),value:n(`botverification.peer.${_.PeerType}`)}),(0,H.jsx)(Z,{label:n(`common.username`),value:W(_.PeerUsername)||`-`}),(0,H.jsx)(Z,{label:n(`botverification.targetTitle`),value:_.PeerTitle||`-`}),(0,H.jsx)(Z,{label:n(`botverification.targetID`),value:_.PeerID,mono:!0})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`botverification.applicantSection`),text:n(`botverification.applicantHint`),action:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/accounts/${_.ApplicantUserID}`),children:[(0,H.jsx)($e,{size:15}),` `,n(`botverification.openApplicant`)]})}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`common.username`),value:W(_.ApplicantUsername)||`-`}),(0,H.jsx)(Z,{label:n(`botverification.applicantID`),value:_.ApplicantUserID,mono:!0}),(0,H.jsx)(Z,{label:n(`botverification.createdAt`),value:G(_.CreatedAt)||`-`}),(0,H.jsx)(Z,{label:n(`common.updatedAt`),value:G(_.UpdatedAt)||`-`})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`botverification.requestSection`),text:n(`botverification.requestHint`)}),(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`botverification.correlationID`),value:_.CorrelationID||`-`,mono:!0}),(0,H.jsx)(Z,{label:n(`common.status`),value:n(`botverification.status.${_.Status}`)})]}),(0,H.jsx)(Xr,{label:n(`botverification.reason`),children:_.Reason?(0,H.jsx)(`p`,{className:`about-text`,children:_.Reason}):(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.notProvided`)})}),(0,H.jsx)(Xr,{label:n(`botverification.requestedDescription`),children:w?(0,H.jsx)(`p`,{className:`about-text`,children:w}):(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.notProvided`)})}),(0,H.jsx)(Xr,{label:n(`botverification.markPreview`),children:E?(0,H.jsx)(`p`,{className:`about-text`,children:E}):(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.notProvided`)})}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.markPreviewHint`)}),w!==``&&!T&&(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.descriptionIgnoredHint`)})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`botverification.decisionSection`),text:n(`botverification.decisionHint`)}),(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`botverification.decidedBy`),value:_.DecidedBy||`-`}),(0,H.jsx)(Z,{label:n(`botverification.approvedAt`),value:G(_.ApprovedAt)||`-`}),(0,H.jsx)(Z,{label:n(`botverification.rejectedAt`),value:G(_.RejectedAt)||`-`}),(0,H.jsx)(Z,{label:n(`botverification.version`),value:_.Version,mono:!0})]}),(0,H.jsx)(Xr,{label:n(`botverification.decisionReason`),children:_.DecisionReason?(0,H.jsx)(`p`,{className:`about-text`,children:_.DecisionReason}):(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.noDecision`)})}),(0,H.jsx)(Xr,{label:`${n(`botverification.internalNote`)} · ${n(`botverification.adminOnly`)}`,children:_.InternalNote?(0,H.jsx)(`p`,{className:`about-text`,children:_.InternalNote}):(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.notProvided`)})})]})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsxs)(`div`,{className:`dock-title`,children:[(0,H.jsx)(Ge,{size:14}),` `,n(`botverification.actionDock`)]}),!x&&!S&&(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.noActions`)}),(x||S)&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`botverification.internalNote`)}),(0,H.jsx)(`textarea`,{value:a,onChange:e=>o(e.target.value),rows:3,placeholder:n(`botverification.internalNotePlaceholder`)})]}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.internalNoteHint`)})]}),x&&(0,H.jsxs)(H.Fragment,{children:[!y&&(0,H.jsx)(J,{children:n(`botverification.verifierMissing`)}),y&&!y.Enabled&&(0,H.jsx)(J,{children:n(`botverification.verifierDisabledHint`)}),b&&(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.markActiveHint`)}),(0,H.jsxs)(`div`,{className:`action-stack`,children:[(0,H.jsx)(Q,{label:n(`botverification.approve`),icon:(0,H.jsx)(I,{size:15}),tone:`neutral`,path:`/api/botverification/requests/${_.ID}/approve`,payload:D,onDone:A,onError:h}),(0,H.jsx)(Q,{label:n(`botverification.reject`),icon:(0,H.jsx)(te,{size:15}),tone:`warn`,path:`/api/botverification/requests/${_.ID}/reject`,payload:D,onDone:A,onError:h})]}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.approveHint`)}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.rejectHint`)})]}),S&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`div`,{className:`dock-title`,children:[(0,H.jsx)(He,{size:14}),` `,n(`botverification.dangerZone`)]}),(0,H.jsxs)(`div`,{className:`danger-zone`,children:[(0,H.jsx)(Q,{label:n(`botverification.revokeRequest`),icon:(0,H.jsx)(se,{size:15}),tone:`danger`,path:`/api/botverification/requests/${_.ID}/revoke`,payload:D,onDone:A,onError:h}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.revokeRequestHint`)}),!b&&(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`botverification.revokeNoMark`)})]})]})]})})]})}function Xr({label:e,children:t}){return(0,H.jsxs)(`div`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:e}),t]})}function Zr(e){return!e||!e.BotID||e.BotID===`0`?null:e}var Qr=[`draft`,`submitted`,`in_review`,`approved`,`rejected`,`cancelled`],$r=[`bot`,`channel`,`supergroup`,`user`];function ei({navigate:e}){let{t}=U(),[n,r]=(0,g.useState)(`all`),[i,a]=(0,g.useState)(`all`),[o,s]=(0,g.useState)(``),[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!==`all`&&t.set(`target_type`,i),o.trim()&&t.set(`reviewer`,o.trim()),c.trim()&&t.set(`q`,c.trim().replace(/^@/,``)),e&&y&&t.set(`before_id`,y);try{let n=await k.verificationApplications(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.verificationCounts()).counts??{})}catch(e){w(O(e))}}(0,g.useEffect)(()=>{T(!1),E()},[]);function D(){T(!1),E()}return(0,H.jsxs)(K,{title:t(`verification.pageTitle`),eyebrow:t(`verification.eyebrow`),actions:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:D,disabled:x,children:[(0,H.jsx)(Fe,{size:15,className:x?`spin`:``}),` `,t(`common.refresh`)]}),children:[C&&(0,H.jsx)(J,{children:C}),(0,H.jsx)(`div`,{className:`metric-row`,children:Qr.map(e=>(0,H.jsx)(X,{label:t(`verification.status.${e}`),value:m[e]??`0`,mono:!0,tone:ri(e,m[e]??`0`)},e))}),(0,H.jsx)(Ot,{children:(0,H.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),T(!1)},children:[(0,H.jsxs)(`label`,{className:`searchbox`,children:[(0,H.jsx)(Ie,{size:15}),(0,H.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`verification.searchPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.status`)}),(0,H.jsxs)(`select`,{value:n,onChange:e=>r(e.target.value),children:[(0,H.jsx)(`option`,{value:`all`,children:t(`verification.statusAll`)}),Qr.map(e=>(0,H.jsx)(`option`,{value:e,children:t(`verification.status.${e}`)},e))]})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`verification.targetType`)}),(0,H.jsxs)(`select`,{value:i,onChange:e=>a(e.target.value),children:[(0,H.jsx)(`option`,{value:`all`,children:t(`verification.targetTypeAll`)}),$r.map(e=>(0,H.jsx)(`option`,{value:e,children:t(`verification.type.${e}`)},e))]})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`verification.reviewer`)}),(0,H.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`verification.reviewerPlaceholder`)})]}),(0,H.jsxs)(`label`,{className:`field-inline`,children:[(0,H.jsx)(`span`,{children:t(`common.limit`)}),(0,H.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),type:`number`,min:`1`,max:`200`})]}),(0,H.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:x,children:[x?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(Ie,{size:15}),` `,t(`common.search`)]})]})}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:t(`common.id`)}),(0,H.jsx)(`th`,{children:t(`verification.target`)}),(0,H.jsx)(`th`,{children:t(`verification.applicant`)}),(0,H.jsx)(`th`,{children:t(`verification.category`)}),(0,H.jsx)(`th`,{children:t(`common.status`)}),(0,H.jsx)(`th`,{children:t(`verification.submittedAt`)}),(0,H.jsx)(`th`,{children:t(`verification.reviewer`)}),(0,H.jsx)(`th`,{})]})}),(0,H.jsxs)(`tbody`,{children:[f.map(n=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{className:`mono`,children:(0,H.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/verification/${n.ID}`),children:[`#`,n.ID]})}),(0,H.jsxs)(`td`,{children:[(0,H.jsx)(`strong`,{children:ii(n)}),(0,H.jsxs)(`div`,{className:`entity-subtitle mono`,children:[t(`verification.type.${n.TargetType}`),` · `,n.TargetID]}),n.TargetVerified&&(0,H.jsxs)(Y,{tone:`good`,children:[(0,H.jsx)(F,{size:12}),` `,t(`verification.alreadyVerified`)]})]}),(0,H.jsxs)(`td`,{children:[W(n.ApplicantUsername)||n.ApplicantName||`-`,(0,H.jsx)(`div`,{className:`entity-subtitle mono`,children:n.ApplicantUserID})]}),(0,H.jsx)(`td`,{children:n.Category||`-`}),(0,H.jsx)(`td`,{children:(0,H.jsx)(ti,{status:n.Status})}),(0,H.jsx)(`td`,{children:G(n.SubmittedAt)||`-`}),(0,H.jsx)(`td`,{children:n.ReviewerAdminID||`-`}),(0,H.jsx)(`td`,{children:(0,H.jsxs)(`button`,{className:`row-link`,type:`button`,onClick:()=>e(`/verification/${n.ID}`),children:[(0,H.jsx)(Ve,{size:14}),` `,t(`common.detail`),` `,(0,H.jsx)(z,{size:14})]})})]},n.ID)),f.length===0&&(0,H.jsx)(Mt,{colSpan:8})]})]})}),_&&(0,H.jsx)(`div`,{className:`toolbar`,children:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>T(!0),disabled:x,children:[x?(0,H.jsx)(L,{size:15,className:`spin`}):(0,H.jsx)(fe,{size:15}),` `,t(`common.loadMore`)]})})]})}function ti({status:e}){let{t}=U();return(0,H.jsx)(Y,{tone:ni(e),children:t(`verification.status.${e}`)})}function ni(e){return e===`approved`?`good`:e===`submitted`||e===`in_review`?`warn`:e===`rejected`?`danger`:`neutral`}function ri(e,t){return e===`submitted`||e===`in_review`?t!==`0`&&t!==``?`warn`:`neutral`:e===`approved`?`good`:`neutral`}function ii(e){return W(e.TargetUsername)||e.TargetTitle||`#${e.TargetID}`}function ai(e){return e.TargetType===`bot`?`/bots/${e.TargetID}`:e.TargetType===`user`?`/accounts/${e.TargetID}`:`/channels/${e.TargetID}`}function oi({id:e,navigate:t}){let{t:n}=U(),{can:r}=Vt(),[i,a]=(0,g.useState)(null),[o,s]=(0,g.useState)(``),[c,l]=(0,g.useState)(!1),[u,d]=(0,g.useState)(!1),[f,p]=(0,g.useState)(``);async function m(){d(!0),p(``);try{a(await k.verificationApplication(e))}catch(e){p(O(e))}finally{d(!1)}}function h(){l(!1),m()}(0,g.useEffect)(()=>{m()},[e]);function _(e){if(e instanceof v&&e.status===409)return l(!0),m(),n(`verification.conflict`)}if(f&&!i)return(0,H.jsx)(J,{children:f});if(!i)return(0,H.jsx)(Nt,{label:n(`verification.loadingDetail`)});let y=i.application,b=i.events??[],x=i.applicant_controls_target,S=i.target_verified,C=y.Status===`submitted`,w=y.Status===`submitted`||y.Status===`in_review`,T=y.Status===`approved`&&r(`verification.revoke`),E=o.trim();function D(){let e={version:y.Version};return E&&(e.internal_note=E),e}function A(){s(``),l(!1),m()}return(0,H.jsxs)(K,{title:n(`verification.detailTitle`,{id:y.ID}),eyebrow:n(`verification.detailEyebrow`),actions:(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/verification`),children:[(0,H.jsx)(ae,{size:15}),` `,n(`common.backToList`)]}),(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:h,disabled:u,children:[(0,H.jsx)(Fe,{size:15,className:u?`spin`:``}),` `,n(`common.refresh`)]})]}),children:[f&&(0,H.jsx)(J,{children:f}),c&&(0,H.jsx)(J,{children:n(`verification.conflict`)}),(0,H.jsx)(kt,{main:(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`section`,{className:`entity-head`,children:[(0,H.jsxs)(`div`,{children:[(0,H.jsx)(`div`,{className:`entity-title`,children:ii(y)}),(0,H.jsxs)(`div`,{className:`entity-subtitle mono`,children:[`#`,y.ID,` · `,n(`verification.type.${y.TargetType}`),`:`,y.TargetID,` · v`,y.Version]})]}),(0,H.jsxs)(`div`,{className:`entity-badges`,children:[(0,H.jsx)(ti,{status:y.Status}),S&&(0,H.jsxs)(Y,{tone:`good`,children:[(0,H.jsx)(F,{size:12}),` `,n(`verification.alreadyVerified`)]}),(0,H.jsx)(Y,{tone:x?`good`:`danger`,children:n(x?`verification.controlsOk`:`verification.controlsLost`)})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`verification.targetSection`),text:n(`verification.targetHint`),action:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(ai(y)),children:[(0,H.jsx)(ge,{size:15}),` `,n(`verification.openTarget`)]})}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`common.type`),value:n(`verification.type.${y.TargetType}`)}),(0,H.jsx)(Z,{label:n(`common.username`),value:W(y.TargetUsername)||`-`}),(0,H.jsx)(Z,{label:n(`verification.targetTitle`),value:y.TargetTitle||`-`}),(0,H.jsx)(Z,{label:n(`verification.targetID`),value:y.TargetID,mono:!0})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`verification.applicantSection`),text:n(`verification.applicantHint`),action:(0,H.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>t(`/accounts/${y.ApplicantUserID}`),children:[(0,H.jsx)($e,{size:15}),` `,n(`verification.openApplicant`)]})}),(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`common.username`),value:W(y.ApplicantUsername)||`-`}),(0,H.jsx)(Z,{label:n(`common.name`),value:y.ApplicantName||`-`}),(0,H.jsx)(Z,{label:n(`verification.applicantID`),value:y.ApplicantUserID,mono:!0}),(0,H.jsx)(Z,{label:n(`verification.submittedAt`),value:G(y.SubmittedAt)||`-`})]}),x?(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.controlsOkHint`)}):(0,H.jsx)(J,{children:n(`verification.controlsLostHint`)})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`verification.applicationSection`),text:n(`verification.applicationHint`)}),(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`verification.category`),value:y.Category||`-`}),(0,H.jsx)(Z,{label:n(`verification.correlationID`),value:y.CorrelationID||`-`,mono:!0}),(0,H.jsx)(Z,{label:n(`verification.createdAt`),value:G(y.CreatedAt)||`-`}),(0,H.jsx)(Z,{label:n(`common.updatedAt`),value:G(y.UpdatedAt)||`-`})]}),(0,H.jsx)(si,{label:n(`verification.description`),children:y.Description?(0,H.jsx)(`p`,{className:`about-text`,children:y.Description}):(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.notProvided`)})}),(0,H.jsx)(si,{label:n(`verification.officialWebsite`),children:y.OfficialWebsite?(0,H.jsx)(`div`,{className:`about-text`,children:(0,H.jsx)(ci,{value:y.OfficialWebsite})}):(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.notProvided`)})}),(0,H.jsx)(si,{label:n(`verification.socialLinks`),children:(0,H.jsx)(li,{values:y.SocialLinks})}),(0,H.jsx)(si,{label:n(`verification.pressLinks`),children:(0,H.jsx)(li,{values:y.PressLinks})}),(0,H.jsx)(si,{label:n(`verification.additionalNote`),children:y.AdditionalNote?(0,H.jsx)(`p`,{className:`about-text`,children:y.AdditionalNote}):(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.notProvided`)})}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.linkSafetyHint`)})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`verification.decisionSection`),text:n(`verification.decisionHint`)}),(0,H.jsxs)(`div`,{className:`stacked-sections`,children:[(0,H.jsxs)(`div`,{className:`summary-grid`,children:[(0,H.jsx)(Z,{label:n(`verification.reviewer`),value:y.ReviewerAdminID||`-`}),(0,H.jsx)(Z,{label:n(`verification.reviewedAt`),value:G(y.ReviewedAt)||`-`}),(0,H.jsx)(Z,{label:n(`common.status`),value:n(`verification.status.${y.Status}`)}),(0,H.jsx)(Z,{label:n(`verification.version`),value:y.Version,mono:!0})]}),(0,H.jsx)(si,{label:n(`verification.decisionReason`),children:y.DecisionReason?(0,H.jsx)(`p`,{className:`about-text`,children:y.DecisionReason}):(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.noDecision`)})}),(0,H.jsx)(si,{label:`${n(`verification.internalNote`)} · ${n(`verification.adminOnly`)}`,children:y.InternalNote?(0,H.jsx)(`p`,{className:`about-text`,children:y.InternalNote}):(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.notProvided`)})})]})]}),(0,H.jsxs)(`section`,{className:`section-block`,children:[(0,H.jsx)(q,{title:n(`verification.eventsSection`),text:n(`verification.eventsHint`)}),(0,H.jsx)(`div`,{className:`table-wrap`,children:(0,H.jsxs)(`table`,{className:`data-table`,children:[(0,H.jsx)(`thead`,{children:(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`th`,{children:n(`verification.eventKind`)}),(0,H.jsx)(`th`,{children:n(`verification.transition`)}),(0,H.jsx)(`th`,{children:n(`audit.actor`)}),(0,H.jsx)(`th`,{children:n(`audit.reason`)}),(0,H.jsx)(`th`,{children:n(`verification.eventNote`)}),(0,H.jsx)(`th`,{children:n(`common.time`)})]})}),(0,H.jsxs)(`tbody`,{children:[b.map(e=>(0,H.jsxs)(`tr`,{children:[(0,H.jsx)(`td`,{children:(0,H.jsx)(ui,{kind:e.Kind})}),(0,H.jsxs)(`td`,{className:`mono`,children:[e.FromStatus||`-`,` → `,e.ToStatus||`-`]}),(0,H.jsx)(`td`,{children:e.Actor||`-`}),(0,H.jsx)(`td`,{className:`truncate`,children:e.Reason||`-`}),(0,H.jsx)(`td`,{className:`truncate`,children:e.Note||`-`}),(0,H.jsx)(`td`,{children:G(e.CreatedAt)||`-`})]},e.ID)),b.length===0&&(0,H.jsx)(Mt,{colSpan:6})]})]})})]})]}),side:(0,H.jsxs)(`section`,{className:`action-dock`,children:[(0,H.jsx)(`div`,{className:`dock-title`,children:n(`verification.actionDock`)}),!C&&!w&&!T&&(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.noActions`)}),C&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsx)(`div`,{className:`action-stack`,children:(0,H.jsx)(Q,{label:n(`verification.claim`),icon:(0,H.jsx)(B,{size:15}),tone:`neutral`,path:`/api/verification/applications/${y.ID}/claim`,payload:()=>({version:y.Version}),onDone:A,onError:_})}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.claimHint`)})]}),(w||T)&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`label`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:n(`verification.internalNote`)}),(0,H.jsx)(`textarea`,{value:o,onChange:e=>s(e.target.value),rows:3,placeholder:n(`verification.internalNotePlaceholder`)})]}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.internalNoteHint`)})]}),w&&(0,H.jsxs)(H.Fragment,{children:[!x&&(0,H.jsx)(J,{children:n(`verification.controlsLostHint`)}),S&&(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.alreadyVerifiedHint`)}),(0,H.jsxs)(`div`,{className:`action-stack`,children:[(0,H.jsx)(Q,{label:n(`verification.approve`),icon:(0,H.jsx)(I,{size:15}),tone:`neutral`,path:`/api/verification/applications/${y.ID}/approve`,payload:D,onDone:A,onError:_}),(0,H.jsx)(Q,{label:n(`verification.reject`),icon:(0,H.jsx)(te,{size:15}),tone:`warn`,path:`/api/verification/applications/${y.ID}/reject`,payload:D,onDone:A,onError:_})]}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.approveHint`)}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.rejectHint`)})]}),T&&(0,H.jsxs)(H.Fragment,{children:[(0,H.jsxs)(`div`,{className:`dock-title`,children:[(0,H.jsx)(He,{size:14}),` `,n(`verification.dangerZone`)]}),(0,H.jsxs)(`div`,{className:`danger-zone`,children:[(0,H.jsx)(Q,{label:n(`verification.revoke`),icon:(0,H.jsx)(se,{size:15}),tone:`danger`,path:`/api/actions/revoke-verification`,payload:()=>{let e={target_type:y.TargetType,target_id:y.TargetID};return E&&(e.internal_note=E),e},onDone:A,onError:_}),(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.revokeHint`)}),!S&&(0,H.jsx)(`p`,{className:`bot-create-note`,children:n(`verification.revokeNotVerified`)})]})]})]})})]})}function si({label:e,children:t}){return(0,H.jsxs)(`div`,{className:`duration-field`,children:[(0,H.jsx)(`span`,{children:e}),t]})}function ci({value:e}){let t=gt(e);return t?(0,H.jsxs)(`a`,{className:`row-link`,href:t,target:`_blank`,rel:`noopener noreferrer`,children:[e,` `,(0,H.jsx)(ge,{size:13})]}):(0,H.jsx)(`span`,{className:`mono`,children:e})}function li({values:e}){let{t}=U(),n=(e??[]).filter(e=>e.trim()!==``);return n.length===0?(0,H.jsx)(`p`,{className:`bot-create-note`,children:t(`verification.notProvided`)}):(0,H.jsx)(`div`,{className:`about-text`,children:n.map((e,t)=>(0,H.jsx)(`div`,{children:(0,H.jsx)(ci,{value:e})},`${t}-${e}`))})}function ui({kind:e}){let{t}=U();return(0,H.jsx)(Y,{tone:e===`approved`?`good`:e===`rejected`||e===`revoked`||e===`cancelled`?`danger`:e===`submitted`||e===`claimed`?`warn`:`neutral`,children:t(`verification.kind.${e}`)})}function di({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(/^\/account-ratings\/(\d+)$/)?.[1],c=e.path.match(/^\/verification\/(\d+)$/)?.[1],l=e.path.match(/^\/bot-verification\/(\d+)$/)?.[1];return l?(0,H.jsx)(Ut,{permission:Lt,children:(0,H.jsx)(Yr,{id:l,navigate:t})}):e.path===`/bot-verification`?(0,H.jsx)(Ut,{permission:Lt,children:(0,H.jsx)(Rr,{navigate:t})}):c?(0,H.jsx)(Ut,{permission:It,children:(0,H.jsx)(oi,{id:c,navigate:t})}):e.path===`/verification`?(0,H.jsx)(Ut,{permission:It,children:(0,H.jsx)(ei,{navigate:t})}):o?(0,H.jsx)(Fn,{id:o,navigate:t}):s?(0,H.jsx)(Sn,{userID:s,navigate:t}):e.path===`/collectible-usernames`?(0,H.jsx)(jn,{navigate:t}):e.path===`/account-ratings`?(0,H.jsx)(vn,{navigate:t}):n?(0,H.jsx)(gn,{id:Number(n),navigate:t}):r?(0,H.jsx)(Rn,{id:Number(r),navigate:t}):i?(0,H.jsx)(Bn,{id:Number(i),navigate:t}):a?(0,H.jsx)(jr,{id:Number(a),navigate:t}):e.path===`/accounts`?(0,H.jsx)(Dn,{navigate:t}):e.path===`/channels`?(0,H.jsx)(zn,{navigate:t}):e.path===`/bots`?(0,H.jsx)(Vn,{navigate:t}):e.path===`/moderation`?(0,H.jsx)(Er,{navigate:t}):e.path===`/emoji`?(0,H.jsx)(Jn,{}):e.path===`/gifts`?(0,H.jsx)(yr,{}):e.path===`/give-gifts`?(0,H.jsx)(Cr,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,H.jsx)($n,{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,H.jsx)(Zn,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups`?(0,H.jsx)(Qn,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,H.jsx)(er,{navigate:t}):(0,H.jsx)(Yn,{navigate:t})}function fi(){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,H.jsx)(nn,{}):e===null?(0,H.jsx)(on,{onLogin:t}):(0,H.jsx)(Bt,{permissions:e.permissions??[],children:(0,H.jsx)(rn,{actor:e.actor,route:n,navigate:i,onLogout:()=>t(null),children:(0,H.jsx)(di,{route:n,navigate:i})})})}_.createRoot(document.getElementById(`root`)).render((0,H.jsx)(g.StrictMode,{children:(0,H.jsx)(Zt,{children:(0,H.jsx)(st,{children:(0,H.jsx)(fi,{})})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/assets/index-KZOn7Xwd.css b/cmd/telesrv-admin/web/dist/assets/index-KZOn7Xwd.css new file mode 100644 index 00000000..53623455 --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-KZOn7Xwd.css @@ -0,0 +1 @@ +:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#eef1f5;--bg-accent:#e7ecf1;--panel:#fff;--panel-subtle:#f5f8fb;--panel-strong:#eef2f6;--surface-soft:#f2f7f6;--overlay:#18222f6b;--topbar-bg:#ffffffdb;--line:#e5eaf0;--line-strong:#d3dce4;--heading:#253040;--text:#333f4d;--text-soft:#45525f;--muted:#6d7885;--muted-2:#9aa4b1;--brand:#1f7d6f;--brand-strong:#196155;--brand-2:#3a6cae;--brand-tint:#e8f4f0;--brand-tint-border:#c8e2db;--brand-tint-text:#235d53;--good:#1f8a57;--good-tint:#eaf6ef;--good-border:#c1e1cf;--warn:#a86a12;--warn-tint:#fcf4e4;--warn-border:#e7d09e;--danger:#c0392b;--danger-tint:#fcefec;--danger-border:#eecac3;--danger-text:#8f2f27;--purple:#6a4fa3;--purple-tint:#f4effb;--purple-border:#dcd0f0;--purple-text:#5a4590;--input-bg:#fff;--btn-bg:#fff;--btn-text:#29323d;--btn-hover:#f4f7fa;--switch-track:#c8d0d6;--code-bg:#1b2733;--code-text:#d6e3ef;--code-border:#2b3a49;--sidebar:#1c2530;--sidebar-soft:#26313d;--sidebar-line:#313c4a;--sidebar-row:#232d38;--sidebar-text:#dbe3ec;--sidebar-muted:#8b98a8;--sidebar-faint:#7c8a9a;--sidebar-heading:#fff;--focus:#1f7d6f29;--shadow:0 12px 34px #1827381a;--shadow-sm:0 2px 10px #1827380d;--shadow-brand:0 8px 22px #1f7d6f38;--radius-xs:8px;--radius-sm:9px;--radius:11px;--radius-lg:14px}[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:#0f141a;--bg-accent:#131a22;--panel:#171f28;--panel-subtle:#1c2530;--panel-strong:#212c38;--surface-soft:#1a232d;--overlay:#05080c9e;--topbar-bg:#151c24db;--line:#29333f;--line-strong:#38434f;--heading:#eef3f8;--text:#d5dde6;--text-soft:#c2ccd6;--muted:#98a4b1;--muted-2:#6d7885;--brand:#37a596;--brand-strong:#45b6a6;--brand-2:#6fa8e6;--brand-tint:#14322d;--brand-tint-border:#245349;--brand-tint-text:#7fd3c4;--good:#47c281;--good-tint:#12301f;--good-border:#245639;--warn:#e0aa4d;--warn-tint:#322810;--warn-border:#574413;--danger:#e6695c;--danger-tint:#35201d;--danger-border:#5c332d;--danger-text:#f0a49b;--purple:#ac90e2;--purple-tint:#221b31;--purple-border:#3d3357;--purple-text:#c9b6ef;--input-bg:#131a22;--btn-bg:#1e2731;--btn-text:#dbe2ea;--btn-hover:#26313d;--switch-track:#3a454f;--code-bg:#0c1218;--code-text:#cdd9e5;--code-border:#232f3b;--sidebar:#10151b;--sidebar-soft:#1c242f;--sidebar-line:#262f3a;--sidebar-row:#161d25;--sidebar-text:#cbd4de;--sidebar-muted:#7c8794;--sidebar-faint:#6f7b88;--sidebar-heading:#f0f4f8;--focus:#37a5963d;--shadow:0 16px 40px #00000075;--shadow-sm:0 2px 12px #00000061;--shadow-brand:0 8px 22px #37a59642}*{box-sizing:border-box}html,body,#root{min-height:100%}body{color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;margin:0;font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;transition:background-color .2s,color .2s}button,input,select,textarea{font:inherit}a{color:inherit;text-decoration:none}.shell{grid-template-columns:232px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{height:100vh;color:var(--sidebar-text);background:var(--sidebar);border-right:1px solid var(--sidebar-line);flex-direction:column;gap:16px;padding:18px 12px;display:flex;position:sticky;top:0;overflow-y:auto}.brand{align-items:center;gap:10px;min-height:42px;padding:0 4px;display:flex}.brand.compact{justify-content:center}.brand-elevated .brand-mark{box-shadow:var(--shadow-brand)}.brand-mark{color:#fff;background:var(--brand);border-radius:var(--radius-sm);border:1px solid #fff3;place-items:center;width:34px;height:34px;font-weight:800;display:grid}.brand strong{font-size:14px;line-height:1.1;display:block}.brand small{color:var(--sidebar-muted);margin-top:3px;font-size:11px;display:block}.sidebar-label{color:var(--sidebar-faint);text-transform:uppercase;letter-spacing:.04em;padding:0 8px;font-size:11px;font-weight:700}.nav-list,.nav-section{gap:4px;display:grid}.nav-section-toggle{width:100%;min-height:38px;color:var(--sidebar-muted);border-radius:var(--radius-sm);cursor:pointer;text-align:left;background:0 0;border:1px solid #0000;grid-template-columns:18px minmax(0,1fr) 16px;align-items:center;gap:9px;padding:0 10px;font-size:12px;font-weight:800;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-section-toggle:hover,.nav-section.active .nav-section-toggle{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-section-chevron{color:var(--sidebar-muted);justify-self:end;transition:transform .14s}.nav-section.open .nav-section-chevron{transform:rotate(180deg)}.nav-children{gap:4px;padding:2px 0 2px 18px;display:grid}.nav-item{min-height:38px;color:var(--sidebar-text);border-radius:var(--radius-sm);border:1px solid #0000;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:9px;padding:0 10px;transition:color .14s,background-color .14s,border-color .14s;display:grid}.nav-dot{background:var(--sidebar-faint);border-radius:999px;justify-self:center;width:6px;height:6px}.nav-item:hover,.nav-item.active{color:var(--sidebar-heading);background:var(--sidebar-soft);border-color:var(--sidebar-line)}.nav-item.active .nav-dot{background:var(--brand)}.sidebar-status{gap:7px;margin-top:auto;display:grid}.runtime-row{min-height:32px;color:var(--sidebar-text);background:var(--sidebar-row);border:1px solid var(--sidebar-line);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;padding:0 8px;display:grid}.runtime-row strong{color:var(--sidebar-heading);font-size:11px}.workspace{min-width:0}.topbar{z-index:20;background:var(--topbar-bg);border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);justify-content:space-between;align-items:center;gap:18px;min-height:66px;padding:12px 24px;display:flex;position:sticky;top:0}.topbar h1{color:var(--heading);margin:2px 0 0;font-size:20px;line-height:1.2}.topbar-actions,.page-actions,.section-action,.entity-badges,.row-actions,.modal-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.language-switch{background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;min-height:30px;padding:2px;display:inline-flex}.language-switch button{min-width:42px;min-height:24px;color:var(--muted);cursor:pointer;background:0 0;border:0;border-radius:999px;padding:0 9px;font-weight:800;transition:color .14s,background-color .14s}.language-switch button.active{color:#fff;background:var(--brand)}.language-switch button:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.theme-toggle{width:34px;height:34px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);cursor:pointer;border-radius:999px;place-items:center;transition:color .16s,background-color .16s,border-color .16s;display:inline-grid}.theme-toggle:hover{color:var(--brand);border-color:var(--brand-tint-border);background:var(--brand-tint)}.theme-toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.actor-pill{min-height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;padding:0 10px;display:inline-flex}.content{gap:16px;padding:18px 24px 30px;display:grid}.eyebrow{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:11px;font-weight:800}.dashboard-layout,.stacked-sections{gap:14px;display:grid}.overview-band,.page-frame{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm)}.overview-band{grid-template-columns:minmax(220px,1fr) minmax(420px,.9fr);align-items:center;gap:16px;padding:16px;display:grid}.overview-band h2,.page-title-row h2,.section-head h2,.modal h2{color:var(--heading);margin:0;font-size:18px;line-height:1.25}.overview-metrics,.metric-row{grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px;display:grid}.overview-metrics{grid-template-columns:repeat(3,minmax(120px,1fr))}.status-item,.metric,.summary-item{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);min-width:0;padding:10px}.status-item span,.metric span,.summary-item span{color:var(--muted);margin-bottom:6px;font-size:11px;display:block}.status-item strong,.metric strong,.summary-item strong{overflow-wrap:anywhere;color:var(--text);font-weight:800;display:block}.status-item.good,.metric.good{border-color:var(--good-border)}.status-item.warn,.metric.warn{border-color:var(--warn-border)}.metric.danger{border-color:var(--danger-border)}.command-grid{grid-template-columns:repeat(3,minmax(220px,1fr));gap:12px;display:grid}.launcher{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-height:94px;box-shadow:var(--shadow-sm);grid-template-columns:38px minmax(0,1fr) 18px;align-items:center;gap:12px;padding:14px;transition:border-color .16s,box-shadow .16s,transform .16s;display:grid}.launcher:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.launcher-icon{width:38px;height:38px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);place-items:center;display:grid}.launcher-copy{gap:4px;display:grid}.launcher-copy strong{color:var(--heading);font-size:15px}.launcher-copy span{color:var(--muted)}.work-strip{grid-template-columns:repeat(4,minmax(160px,1fr));gap:8px;display:grid}.strip-item{min-height:38px;color:var(--text-soft);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.page-frame{gap:14px;padding:14px;display:grid}.page-title-row{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:14px;padding-bottom:12px;display:flex}.query-panel{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);padding:10px}.toolbar{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.message-query input{width:150px}.message-selector-grid{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;margin-bottom:10px;display:grid}.message-selector-grid.single{grid-template-columns:minmax(320px,620px)}.entity-picker{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;min-width:0;padding:10px;display:grid}.picker-head{min-height:24px;color:var(--text-soft);justify-content:space-between;align-items:center;gap:8px;font-weight:800;display:flex}.selected-entity{min-height:40px;color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:8px;padding:7px 9px;display:grid}.selected-entity strong,.selected-entity span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.selected-entity div{gap:2px;min-width:0;display:grid}.selected-entity div span{color:var(--brand-tint-text);opacity:.85;font-size:11px}.picker-search{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:var(--radius-sm);grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;height:34px;padding:0 6px 0 9px;display:grid}.picker-search input{width:100%;height:30px;box-shadow:none;background:0 0;border:0;padding:0}.picker-results{border:1px solid var(--line);border-radius:var(--radius-sm);max-height:236px;display:grid;overflow:auto}.picker-row{min-height:36px;color:var(--text);background:var(--panel);border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;grid-template-columns:96px minmax(120px,1fr) minmax(120px,1fr) auto;align-items:center;gap:8px;padding:6px 8px;display:grid}.picker-row:last-child{border-bottom:0}.picker-row:hover,.picker-row.selected{background:var(--surface-soft)}.picker-row strong,.picker-row span{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.picker-empty,.picker-error{color:var(--muted);text-align:center;padding:9px}.picker-error{color:var(--danger);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius-sm)}input,select,textarea{color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);outline:none;transition:border-color .14s,box-shadow .14s}input::placeholder,textarea::placeholder{color:var(--muted-2)}input,select{width:190px;height:34px;padding:0 10px}select{min-width:220px;height:34px;font:inherit;appearance:none;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239aa4b2' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-position:right 10px center;background-repeat:no-repeat;padding:0 30px 0 10px;font-weight:600}select:disabled{color:var(--muted-2);cursor:not-allowed}textarea{resize:vertical;width:100%;padding:9px 10px}input:focus,select:focus,textarea:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus)}.small-input{width:88px}.field-inline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.field-inline span{font-size:11px;font-weight:700}.searchbox{width:min(380px,100%);height:34px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:inline-flex}.searchbox input{width:100%;height:30px;box-shadow:none;border:0;padding:0}.btn{min-height:34px;color:var(--btn-text);background:var(--btn-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:6px;padding:0 12px;transition:background-color .14s,border-color .14s,color .14s,box-shadow .14s;display:inline-flex}.btn:hover:not(:disabled){background:var(--btn-hover)}.btn:disabled{color:var(--muted-2);cursor:not-allowed}.btn.primary{color:#fff;background:var(--brand);border-color:var(--brand)}.btn.primary:hover:not(:disabled){background:var(--brand-strong);border-color:var(--brand-strong)}.btn.ghost{background:var(--panel-subtle)}.btn.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.btn.danger:hover:not(:disabled){background:var(--danger-tint);border-color:var(--danger)}.btn.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.btn.warn:hover:not(:disabled){background:var(--warn-tint);border-color:var(--warn)}.btn:disabled,.btn.primary:disabled,.btn.warn:disabled,.btn.danger:disabled{color:var(--muted-2);background:var(--panel-strong);border-color:var(--line);cursor:not-allowed}.btn.full{width:100%}.icon-text{gap:7px}.compact-btn{min-height:28px;padding:0 8px;font-size:12px}.row-link,.link-button{color:var(--brand-2);cursor:pointer;background:0 0;border:0;align-items:center;gap:4px;padding:0;display:inline-flex}.table-wrap{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);width:100%;overflow-x:auto}.data-table{border-collapse:collapse;width:100%;font-size:12.5px}.data-table th,.data-table td{border-bottom:1px solid var(--line);text-align:left;vertical-align:middle;white-space:nowrap;height:38px;padding:7px 9px}.data-table th{z-index:0;color:var(--muted);background:var(--panel-strong);font-weight:800;position:sticky;top:0}.data-table tbody tr:hover{background:var(--panel-subtle)}.data-table tr:last-child td{border-bottom:0}.mono{font-family:SFMono-Regular,Consolas,Liberation Mono,monospace}.truncate{text-overflow:ellipsis;max-width:380px;overflow:hidden}.badge{min-height:22px;color:var(--muted);background:var(--panel-strong);border:1px solid var(--line-strong);white-space:nowrap;border-radius:999px;align-items:center;padding:1px 8px;display:inline-flex}.badge.good{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.badge.danger{color:var(--danger);background:var(--danger-tint);border-color:var(--danger-border)}.badge.warn{color:var(--warn);background:var(--warn-tint);border-color:var(--warn-border)}.empty-cell{color:var(--muted);text-align:center}.bot-create-fields{grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;display:grid}.bot-create-fields .duration-field input{width:100%}.bot-create-actions{border-top:1px solid var(--line);justify-content:space-between;align-items:center;gap:14px;margin-top:14px;padding-top:14px;display:flex}.bot-create-note{color:var(--muted);font-size:12px;line-height:1.4}@media (width<=760px){.bot-create-fields{grid-template-columns:1fr}.bot-create-actions{flex-direction:column;align-items:stretch}}.progress-cell{gap:4px;min-width:130px;display:grid}.progress-cell small,.progress-note{color:var(--muted);font-size:11px}.progress-bar{background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;width:100%;height:6px;overflow:hidden}.progress-bar>span{background:var(--brand-2);height:100%;display:block}.progress-bar.good>span{background:var(--good)}.progress-bar.danger>span{background:var(--danger)}.progress-wide .progress-cell{min-width:0}.split-layout{grid-template-columns:minmax(0,1fr) 330px;align-items:start;gap:14px;display:grid}.split-main,.split-side{min-width:0}.entity-head{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);justify-content:space-between;align-items:flex-start;gap:14px;padding:14px;display:flex}.entity-title{color:var(--heading);font-size:20px;font-weight:800;line-height:1.25}.entity-subtitle{color:var(--muted);margin-top:4px}.summary-grid{grid-template-columns:repeat(4,minmax(150px,1fr));gap:8px;display:grid}.about-text{color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);margin:0;padding:10px}.section-block,.action-dock,.surface{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);min-width:0;box-shadow:var(--shadow-sm);padding:12px}.section-head{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:10px;display:flex}.section-head p{color:var(--muted);margin:5px 0 0}.action-dock{gap:10px;display:grid;position:sticky;top:82px}.dock-title{color:var(--text-soft);border-bottom:1px solid var(--line);padding-bottom:4px;font-weight:800}.action-dock>.btn,.action-dock .action-stack .btn{justify-content:center;width:100%}.duration-field{gap:4px;display:grid}.duration-field span{color:var(--muted);font-size:11px;font-weight:800}.duration-field input,.duration-field select{width:100%}.action-stack{gap:10px;display:grid}.action-stack .btn,.action-dock>.btn{min-height:42px}.danger-zone{border-top:1px solid var(--line);flex-wrap:wrap;gap:8px;margin-top:10px;padding-top:10px;display:flex}.dock-title+.danger-zone{border-top:0;margin-top:0;padding-top:0}.authorization-block{gap:10px;display:grid}.authorization-table{table-layout:fixed;min-width:720px}.authorization-table th,.authorization-table td{height:46px}.device-text{text-overflow:ellipsis;max-width:260px;overflow:hidden}.device-actions-head{width:190px}.device-actions-cell{width:190px;min-width:190px}.device-actions{white-space:normal;grid-template-columns:repeat(2,minmax(82px,1fr));gap:6px;min-width:178px;display:grid}.device-actions .btn{justify-content:center;width:100%}.operation-row{grid-template-columns:repeat(2,minmax(280px,1fr));gap:10px;display:grid}.operation-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);flex-wrap:wrap;align-items:center;gap:8px;padding:10px;display:flex}.operation-title{width:100%;color:var(--heading);align-items:center;gap:6px;font-weight:800;display:flex}.checkline{color:var(--muted);align-items:center;gap:6px;display:inline-flex}.checkline input{width:auto;height:auto}.alert{color:var(--danger-text);background:var(--danger-tint);border:1px solid var(--danger-border);border-radius:var(--radius);align-items:flex-start;gap:8px;padding:9px 10px;display:flex}.json-block{max-height:520px;color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius);margin:0;padding:12px;font-size:12px;overflow:auto}.raw-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;display:grid}.loading-line{min-height:80px;color:var(--muted);place-items:center;display:grid}.empty-panel{min-height:92px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);place-items:center;display:grid}.gift-metrics .metric{background:var(--panel-subtle);min-height:68px;padding:12px}.gift-metrics .metric strong{font-size:17px}.gift-file-icon{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);flex:none;place-items:center;display:grid}.gift-format-chips{flex-wrap:wrap;flex:none;justify-content:flex-end;gap:6px;display:flex}.gift-format-chips span{color:var(--brand-tint-text);background:var(--brand-tint);border:1px solid var(--brand-tint-border);letter-spacing:.02em;border-radius:999px;padding:4px 8px;font-size:10px;font-weight:800}.gift-list-summary{color:var(--muted);margin-left:auto;font-size:11px;font-weight:700}.gift-import-modal{width:min(860px,100%)}.gift-import-modal-body{gap:14px}.gift-source-tabs{gap:8px;display:flex}.give-gift-summary{background:var(--panel-subtle);border:1px solid var(--line-strong);color:var(--text-soft);border-radius:12px;align-items:center;gap:11px;padding:11px 13px;display:flex}.give-gift-summary>svg{color:var(--brand);flex:none}.give-gift-summary strong{color:var(--text);font-size:13px;display:block}.give-gift-summary .mono{color:var(--muted);font-size:11px}.give-gift-tabs{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:12px;gap:4px;width:100%;padding:4px;display:flex}.give-gift-tabs .btn{min-height:36px;box-shadow:none;color:var(--text-soft);background:0 0;border:1px solid #0000;border-radius:9px;flex:1 1 0;justify-content:center;transition:color .15s,background .15s,border-color .15s,box-shadow .15s}.give-gift-tabs .btn:not(.primary):hover{color:var(--brand);background:var(--brand-tint)}.give-gift-tabs .btn.primary{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.give-gift-upgrade-note{background:var(--brand-tint);border:1px solid var(--brand-tint-border);color:var(--text-soft);border-radius:10px;margin:0;padding:9px 12px;font-size:11px;font-weight:650;line-height:1.45}.give-gift-attrs{grid-template-columns:repeat(3,minmax(0,1fr));align-items:end}.give-gift-attrs select,.give-gift-attrs input{width:100%;min-width:0;height:38px;color:var(--text);background-color:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;appearance:none;cursor:pointer;padding:0 32px 0 10px;font-size:12px;font-weight:600}.give-gift-attrs input{cursor:text;text-overflow:ellipsis;padding-right:10px}.give-gift-attrs select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239aa4b2' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-position:right 11px center;background-repeat:no-repeat}.give-gift-attrs select:focus,.give-gift-attrs input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.give-gift-layout{grid-template-columns:minmax(220px,280px) minmax(0,1fr);align-items:start;gap:16px;display:grid}.give-gift-picker{align-content:start;gap:10px;display:grid}.give-gift-picker-head{align-items:center;gap:12px;display:flex}.give-gift-picker-head .searchbox{flex:auto}.give-gift-picker-list{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-lg);gap:8px;max-height:640px;padding:8px;display:grid;overflow-y:auto}.give-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);grid-template-columns:46px minmax(0,1fr) auto;align-items:center;gap:11px;padding:9px 11px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.give-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.give-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.give-gift-thumb{place-items:center;width:46px;height:46px;display:grid}.give-gift-thumb canvas{width:100%!important;height:100%!important}.give-gift-option-info{gap:3px;min-width:0;display:grid}.give-gift-option-info strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.give-gift-option-info .mono{color:var(--muted);font-size:10px}.give-gift-option-price{white-space:nowrap;justify-self:end}.give-gift-panel{background:var(--panel);border:1px solid var(--line-strong);border-radius:var(--radius-lg);gap:12px;min-width:0;padding:16px;display:grid}.give-gift-form{gap:12px;min-width:0;display:grid}.give-gift-form-actions{flex-wrap:wrap;justify-content:flex-end;gap:10px;padding-top:4px;display:flex}.give-gift-empty-panel{color:var(--muted);text-align:center;place-items:center;gap:10px;padding:48px 20px;display:grid}.give-gift-empty-panel svg{color:var(--brand);opacity:.8}.official-gift-picker{gap:12px;min-width:0;display:grid}.official-gift-tools{align-items:center;gap:12px;display:flex}.official-gift-tools .searchbox{width:100%}.official-gift-tools>span{color:var(--muted);flex:none;font-size:11px;font-weight:750}.official-gift-categories{flex-wrap:wrap;gap:7px;display:flex}.official-gift-categories button{min-height:32px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line-strong);font:inherit;cursor:pointer;border-radius:999px;align-items:center;gap:7px;padding:5px 10px;font-size:11px;font-weight:800;transition:color .15s,background .15s,border-color .15s,box-shadow .15s;display:inline-flex}.official-gift-categories button:hover{color:var(--brand);border-color:var(--brand-tint-border)}.official-gift-categories button.active{color:#fff;background:var(--brand);border-color:var(--brand);box-shadow:var(--shadow-brand)}.official-gift-categories button span{min-width:20px;height:20px;color:inherit;background:#7d8c9b38;border-radius:999px;place-items:center;padding:0 5px;font-size:10px;display:grid}.official-gift-categories button.active span{color:var(--brand);background:#ffffffd9}.official-gift-list{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--panel-subtle);scrollbar-gutter:stable;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;min-height:126px;max-height:314px;padding:8px;display:grid;overflow:auto}.official-gift-option{text-align:left;min-width:0;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);cursor:pointer;box-shadow:var(--shadow-sm);gap:8px;padding:11px 12px;transition:border-color .15s,box-shadow .15s,transform .15s;display:grid}.official-gift-option:hover{border-color:var(--brand-tint-border);box-shadow:var(--shadow);transform:translateY(-1px)}.official-gift-option.selected{border-color:var(--brand);box-shadow:0 0 0 2px var(--focus), var(--shadow)}.official-gift-option-head{grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:8px;display:grid}.official-gift-option-head strong{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.official-gift-option-head .mono{color:var(--muted);font-size:9px}.official-gift-option-meta{color:var(--muted);flex-wrap:wrap;gap:10px;font-size:10px;font-weight:700;display:flex}.official-gift-capabilities{flex-wrap:wrap;gap:5px;display:flex}.official-gift-capabilities>span{letter-spacing:.01em;border:1px solid #0000;border-radius:999px;padding:3px 7px;font-size:9px;font-weight:850}.official-gift-capabilities>span.yes{color:var(--good);background:var(--good-tint);border-color:var(--good-border)}.official-gift-capabilities>span.craft{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.official-gift-capabilities>span.no{color:var(--muted);background:var(--panel-strong);border-color:var(--line-strong)}.official-gift-empty{min-height:108px;color:var(--muted);text-align:center;grid-column:1/-1;place-items:center;padding:20px;font-size:12px;display:grid}.official-gift-selected{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--surface-soft);grid-template-columns:108px minmax(0,1fr);align-items:center;gap:14px;padding:12px;display:grid}.official-gift-selected .gift-animation-shell{width:96px;height:96px}.official-gift-selected>div:last-child{gap:5px;min-width:0;display:grid}.official-gift-selected small{color:var(--muted)}.gift-import-note{color:var(--muted);justify-content:space-between;align-items:center;gap:12px;line-height:1.45;display:flex}.gift-file-picker{min-height:78px;color:var(--text);background:var(--panel);border:1px dashed var(--line-strong);border-radius:var(--radius);cursor:pointer;grid-template-columns:42px minmax(0,1fr) auto;align-items:center;gap:12px;padding:12px 14px;transition:border-color .16s,background .16s,box-shadow .16s;display:grid;position:relative}.gift-file-picker:hover,.gift-file-picker.has-file{background:var(--brand-tint);border-color:var(--brand);box-shadow:0 0 0 2px var(--focus)}.gift-file-picker input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.gift-file-icon{border-radius:var(--radius-sm);width:40px;height:40px}.gift-file-copy{gap:2px;min-width:0;display:grid}.gift-field-label{color:var(--muted);text-transform:uppercase;letter-spacing:.04em;font-size:10px;font-weight:800}.gift-file-copy strong{color:var(--heading);text-overflow:ellipsis;white-space:nowrap;font-size:13px;overflow:hidden}.gift-file-copy small{color:var(--muted);font-size:11px;font-weight:500}.gift-file-action{color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:var(--radius-sm);padding:7px 10px;font-size:11px;font-weight:800}.gift-fields-grid{grid-template-columns:minmax(200px,1.5fr) repeat(3,minmax(120px,1fr));gap:10px;display:grid}.gift-fields-grid label,.gift-reason-field{color:var(--muted);gap:6px;font-size:11px;font-weight:700;display:grid}.gift-fields-grid input,.gift-reason-field input{width:100%;min-width:0;height:38px;color:var(--text);background:var(--input-bg);border:1px solid var(--line);border-radius:var(--radius-sm);padding:0 10px}.gift-fields-grid input:focus,.gift-reason-field input:focus{border-color:var(--brand);box-shadow:0 0 0 3px var(--focus);outline:none}.gift-switch{color:var(--text-soft);cursor:pointer;align-items:center;gap:9px;font-size:12px;font-weight:700;display:inline-flex}.gift-switch input{opacity:0;width:1px;height:1px;position:absolute}.gift-switch-track{background:var(--switch-track);border-radius:999px;align-items:center;width:34px;height:19px;padding:2px;transition:background .16s;display:flex}.gift-switch-track span{background:#fff;border-radius:50%;width:15px;height:15px;transition:transform .16s;box-shadow:0 1px 3px #10182838}.gift-switch input:checked+.gift-switch-track{background:var(--brand)}.gift-switch input:checked+.gift-switch-track span{transform:translate(15px)}.gift-switch input:focus-visible+.gift-switch-track{outline:3px solid var(--focus);outline-offset:2px}.gift-validation{color:var(--code-text);background:var(--code-bg);border:1px solid var(--code-border);border-radius:var(--radius-sm);overflow:hidden}.gift-validation-head{color:var(--code-text);background:#ffffff09;border-bottom:1px solid #ffffff17;align-items:center;gap:9px;padding:10px 12px;display:flex}.gift-validation-head div{gap:2px;display:grid}.gift-validation-head span{color:var(--brand);font-size:10px}.gift-validation pre{max-height:180px;color:var(--code-text);margin:0;padding:11px 12px;font-size:11px;overflow:auto}.gift-animation-shell{background:var(--surface-soft);place-items:center;min-height:210px;display:grid;position:relative}.gift-animation{width:200px;height:200px}.gift-animation canvas{width:100%!important;height:100%!important}.gift-play{width:30px;height:30px;color:var(--text);background:var(--panel);border:1px solid var(--line);border-radius:50%;place-items:center;display:grid;position:absolute;bottom:8px;right:8px}.gift-table-wrap{background:var(--panel)}.gift-table{min-width:1080px}.gift-table th:first-child{width:74px}.gift-table td{vertical-align:middle}.gift-animation-shell.compact{border:1px solid var(--line);border-radius:var(--radius-sm);width:56px;min-height:56px;overflow:hidden}.gift-animation-shell.compact .gift-animation{width:54px;height:54px}.gift-animation-shell.compact .gift-play{width:20px;height:20px;bottom:3px;right:3px}.gift-row-disabled{opacity:.68}.gift-table-title,.gift-sort-order,.gift-source-size,.gift-convert-price{display:block}.gift-table-title{text-overflow:ellipsis;white-space:nowrap;max-width:220px;overflow:hidden}.gift-sort-order,.gift-source-size,.gift-convert-price{color:var(--muted);margin-top:3px;font-size:10px}.gift-table-price{color:var(--warn)}.gift-table-actions{align-items:center;gap:6px;display:flex}.collectible-button{color:var(--purple);background:var(--purple-tint);border-color:var(--purple-border)}.collectible-button:hover{background:var(--purple-tint);border-color:var(--purple)}.collectible-modal{width:min(1180px,100%);max-height:min(92vh,980px)}.collectible-modal .modal-head p{color:var(--muted);margin:4px 0 0;font-size:11px}.collectible-modal-body{background:var(--bg);gap:16px;padding:16px 18px 22px;overflow:auto}.collectible-loading{min-height:90px;color:var(--muted);justify-content:center;align-items:center;gap:8px;display:flex}.collectible-empty{color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius);align-items:center;gap:12px;padding:16px;display:flex}.collectible-empty div,.collectible-definition-head>div:first-child,.collectible-section-head>div:first-child{gap:3px;display:grid}.collectible-empty span,.collectible-definition-head span,.collectible-section-head span{color:var(--muted);font-size:10px;font-weight:500}.collectible-active{background:var(--panel);border:1px solid var(--purple-border);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-active-head{background:var(--purple-tint);border-bottom:1px solid var(--purple-border);justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.collectible-active-head>div{color:var(--purple-text);align-items:center;gap:9px;display:flex}.collectible-active-head>div>div{gap:2px;display:grid}.collectible-active-head span{color:var(--muted);font-size:10px}.collectible-active-grid{background:var(--line);grid-template-columns:repeat(auto-fill,minmax(145px,1fr));gap:1px;display:grid}.collectible-active-grid article{background:var(--panel);align-items:center;gap:9px;min-width:0;padding:9px 11px;display:flex}.collectible-active-grid article>div:last-child{gap:2px;min-width:0;display:grid}.collectible-active-grid article strong{text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.collectible-active-grid article span{color:var(--muted);font-size:9px}.collectible-definition{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);overflow:hidden}.collectible-definition-head{background:var(--panel-subtle);border-bottom:1px solid var(--line);justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.collectible-main-fields{background:var(--panel-subtle);border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section{border-bottom:1px solid var(--line);padding:14px 16px}.collectible-section:last-child{border-bottom:0}.collectible-section-head{justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;display:flex}.collectible-section-tools{align-items:center;gap:7px;display:flex}.collectible-rows{gap:7px;display:grid}.collectible-row{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:end;gap:7px;padding:9px 9px 9px 36px;display:grid;position:relative}.collectible-row:hover{background:var(--panel);border-color:var(--line-strong);box-shadow:var(--shadow-sm)}.collectible-row.animated{grid-template-columns:minmax(120px,1.2fr) 90px 78px minmax(160px,1.4fr) 48px 30px}.collectible-row.backdrop{grid-template-columns:minmax(110px,1.2fr) 70px 80px 70px repeat(4,52px) 48px 30px}.collectible-row-index{width:27px;color:var(--purple-text);background:var(--purple-tint);border-right:1px solid var(--purple-border);border-radius:var(--radius-xs) 0 0 var(--radius-xs);place-items:center;font-size:10px;font-weight:800;display:grid;position:absolute;top:0;bottom:0;left:0}.collectible-row label{gap:4px;min-width:0;display:grid}.collectible-row label>span{color:var(--muted);text-transform:uppercase;letter-spacing:.025em;font-size:9px;font-weight:800}.collectible-row input:not([type=file]){width:100%;min-width:0;height:32px;color:var(--text);background:var(--input-bg);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font:inherit;padding:0 8px;font-size:11px}.collectible-row input:focus{border-color:var(--purple);box-shadow:0 0 0 3px var(--purple-tint);outline:none}.collectible-file input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.collectible-file em{min-width:0;height:32px;color:var(--purple-text);background:var(--purple-tint);border:1px dashed var(--purple-border);border-radius:var(--radius-sm);text-overflow:ellipsis;white-space:nowrap;cursor:pointer;align-items:center;gap:5px;padding:0 8px;font-size:10px;font-style:normal;font-weight:700;display:flex;overflow:hidden}.collectible-inline-preview{width:42px;height:42px;color:var(--purple);background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);place-items:center;display:grid;overflow:hidden}.collectible-animation{width:100%;height:100%;overflow:hidden}.collectible-animation.compact{background:var(--purple-tint);border:1px solid var(--purple-border);border-radius:var(--radius-sm);flex:0 0 42px;place-items:center;width:42px;height:42px;display:grid}.collectible-animation canvas{width:100%!important;height:100%!important}.collectible-animation.failed{color:var(--danger);background:var(--danger-tint)}.collectible-animation.loading{color:var(--purple-text)}.collectible-file-error{color:var(--danger);grid-column:1/-1;font-size:10px}.collectible-color input{cursor:pointer;height:32px!important;padding:3px!important}.collectible-backdrop-preview{border-radius:var(--radius-sm);border:1px solid #2a1f472e;flex:0 0 42px;place-items:center;width:42px;height:42px;font-size:11px;font-weight:900;display:grid;box-shadow:inset 0 0 0 1px #fff3}.collectible-row .icon-btn{align-self:center}.collectible-row .icon-btn:disabled{opacity:.28}@media (width<=900px){.gift-fields-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.give-gift-layout{grid-template-columns:1fr}.give-gift-picker-list{max-height:320px}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:repeat(2,minmax(0,1fr))}.collectible-inline-preview,.collectible-backdrop-preview,.collectible-row .icon-btn{place-self:center start}}@media (width<=620px){.gift-import-note{flex-direction:column;align-items:flex-start}.gift-format-chips{justify-content:flex-start}.gift-file-picker{grid-template-columns:40px minmax(0,1fr)}.gift-file-action{display:none}.gift-fields-grid{grid-template-columns:1fr}.gift-list-summary{width:100%;margin-left:0}.official-gift-tools{flex-direction:column;align-items:stretch}.official-gift-list{grid-template-columns:1fr;max-height:340px}.official-gift-selected{grid-template-columns:82px minmax(0,1fr)}.official-gift-selected .gift-animation-shell{width:72px;height:72px}.collectible-modal-body{padding:10px}.collectible-definition-head,.collectible-section-head{flex-direction:column;align-items:flex-start}.collectible-row.animated,.collectible-row.backdrop{grid-template-columns:1fr}.collectible-active-grid{grid-template-columns:1fr 1fr}}.attr-block{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);gap:8px;padding:10px;display:grid}.attr-block .duration-field input,.duration-field select{width:100%}.attr-block .btn{justify-content:center;width:100%}.emoji-grid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;display:grid}.emoji-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-sm);gap:8px;padding:12px;display:grid}.emoji-preview{background:var(--surface-soft);border:1px solid var(--line);border-radius:var(--radius-sm);place-items:center;height:88px;display:grid}.emoji-anim{width:80px;height:80px}.emoji-anim canvas{width:100%!important;height:100%!important}.emoji-glyph{font-size:46px;line-height:1}.emoji-meta{gap:4px;min-width:0;display:grid}.emoji-alt{font-size:18px;line-height:1.2}.emoji-id{width:100%;min-width:0;color:var(--text);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;justify-content:space-between;align-items:center;gap:6px;padding:4px 8px;font-size:11px;display:flex}.emoji-id .mono{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.emoji-id svg{flex:none}.emoji-id:hover{border-color:var(--brand-tint-border);color:var(--brand)}.emoji-sub{color:var(--muted);text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.breakdown-list{gap:8px;margin-bottom:10px;display:grid}.breakdown-row{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);grid-template-columns:minmax(140px,260px) 1fr minmax(80px,auto);align-items:center;gap:12px;padding:9px 10px;display:grid}.breakdown-row.total{background:0 0;grid-template-columns:1fr minmax(80px,auto)}.breakdown-label{gap:2px;min-width:0;display:grid}.breakdown-label strong{color:var(--text);font-weight:800}.breakdown-label small{color:var(--muted);font-size:11px;line-height:1.35}.breakdown-value{color:var(--text);text-align:right;font-weight:800}.breakdown-value.good{color:var(--good)}.breakdown-value.danger{color:var(--danger-text)}@media (width<=760px){.breakdown-row,.breakdown-row.total{grid-template-columns:1fr}.breakdown-value{text-align:left}}.username-branch{margin:2px 0 0;padding:0;list-style:none}.username-branch li{color:var(--text-soft);padding-left:14px;font-size:12px;line-height:1.7;position:relative}.username-branch li:before{border-left:1px solid var(--line-strong,var(--line));border-bottom:1px solid var(--line-strong,var(--line));content:"";width:6px;height:11px;position:absolute;top:0;left:3px}.username-branch li.inactive{color:var(--muted)}.username-branch li.inactive span{text-decoration:line-through}.username-branch li em{text-transform:uppercase;letter-spacing:.04em;margin-left:6px;font-size:10px;font-style:normal;font-weight:800}.modal-backdrop{z-index:10000;background:var(--overlay);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(760px,100%);max-height:min(820px,100vh - 48px);box-shadow:var(--shadow);padding:0;overflow:hidden}.command-modal{flex-direction:column;display:flex}.command-modal>.modal-head,.command-modal>.modal-actions{flex:none}.modal-head{border-bottom:1px solid var(--line);justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 18px 12px;display:flex}.icon-btn{width:30px;height:30px;color:var(--text-soft);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);cursor:pointer;place-items:center;transition:background-color .14s,border-color .14s,color .14s;display:grid}.icon-btn:hover{background:var(--btn-hover);border-color:var(--line-strong)}.command-steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.command-body{grid-auto-rows:max-content;gap:12px;min-height:0;padding:14px 18px;display:grid;overflow:auto}.command-step{min-height:38px;color:var(--muted);background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius-sm);align-items:center;gap:8px;padding:0 10px;display:flex}.command-step span{background:var(--panel);border:1px solid var(--line);border-radius:999px;place-items:center;width:20px;height:20px;font-size:11px;font-weight:800;display:grid}.command-step.active{color:var(--brand);border-color:var(--brand-tint-border)}.command-step.done{color:var(--good);border-color:var(--good-border)}.form-field{gap:6px;display:grid}.form-field span,.form-stack span{color:var(--text-soft);font-weight:800}.form-field input:disabled,.form-field textarea:disabled{opacity:.6;cursor:not-allowed}.command-preview{gap:8px;display:grid}.command-preview .json-block{max-height:150px}.preview-head,.result-title{color:var(--text-soft);align-items:center;gap:7px;font-weight:800;display:flex}.result-box{background:var(--panel-subtle);border:1px solid var(--line);border-radius:var(--radius);gap:8px;padding:10px;display:grid}.result-line{grid-template-columns:92px minmax(0,1fr);gap:8px;display:grid}.result-line span{color:var(--muted)}.result-line strong{overflow-wrap:anywhere}.result-message{color:var(--text-soft)}.modal-actions{background:var(--panel);border-top:1px solid var(--line);justify-content:flex-end;padding:12px 18px}.login-page{background:var(--bg);place-items:center;min-height:100vh;padding:24px;display:grid}.login-panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);width:min(420px,100%);box-shadow:var(--shadow);gap:18px;padding:22px;display:grid}.login-head{justify-content:space-between;align-items:center;gap:12px;display:flex}.login-head-actions{flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:8px;display:flex}.login-chip{min-height:24px;color:var(--brand);background:var(--brand-tint);border:1px solid var(--brand-tint-border);border-radius:999px;align-items:center;padding:0 8px;font-size:12px;display:inline-flex}.login-copy h1{color:var(--heading);margin:0;font-size:22px}.login-copy p{color:var(--muted);margin:8px 0 0}.form-stack{gap:12px;display:grid}.form-stack label{gap:6px;display:grid}.form-stack input{width:100%}.boot-screen{background:var(--bg);align-content:center;place-items:center;gap:18px;min-height:100vh;display:grid}.loader-bar{background:var(--line-strong);border-radius:999px;width:180px;height:4px;overflow:hidden}.loader-bar:before{content:"";background:var(--brand);width:42%;height:100%;animation:1s ease-in-out infinite load;display:block}.spin{animation:.8s linear infinite spin}@keyframes load{0%{transform:translate(-120%)}to{transform:translate(260%)}}@keyframes spin{to{transform:rotate(360deg)}}@media (width<=1120px){.shell{grid-template-columns:1fr}.sidebar{height:auto;position:static}.nav-list{grid-template-columns:repeat(4,minmax(0,1fr))}.sidebar-status{display:none}.overview-band,.split-layout,.operation-row,.raw-grid,.message-selector-grid,.message-selector-grid.single{grid-template-columns:1fr}.action-dock{position:static}}@media (width<=760px){.content,.topbar{padding-left:14px;padding-right:14px}.command-grid,.work-strip,.overview-metrics,.metric-row,.summary-grid,.command-steps{grid-template-columns:1fr}.sidebar{gap:12px;padding:14px}.nav-list{grid-template-columns:repeat(2,minmax(0,1fr))}.topbar,.page-title-row,.entity-head{flex-direction:column;align-items:flex-start}input,.searchbox{width:100%}.toolbar{align-items:stretch}.picker-row,.selected-entity{grid-template-columns:1fr}} diff --git a/cmd/telesrv-admin/web/dist/index.html b/cmd/telesrv-admin/web/dist/index.html index c1c0b056..9d18747d 100644 --- a/cmd/telesrv-admin/web/dist/index.html +++ b/cmd/telesrv-admin/web/dist/index.html @@ -21,8 +21,8 @@ } })(); - - + +
diff --git a/cmd/telesrv-admin/web/package-lock.json b/cmd/telesrv-admin/web/package-lock.json index 52c8bdd2..18e4c2de 100644 --- a/cmd/telesrv-admin/web/package-lock.json +++ b/cmd/telesrv-admin/web/package-lock.json @@ -758,9 +758,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -797,9 +797,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "dev": true, "funding": [ { @@ -817,7 +817,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/cmd/telesrv-admin/web/src/App.tsx b/cmd/telesrv-admin/web/src/App.tsx index bbde52f0..deefcc9d 100644 --- a/cmd/telesrv-admin/web/src/App.tsx +++ b/cmd/telesrv-admin/web/src/App.tsx @@ -1,12 +1,16 @@ import { useEffect, useState } from "react"; -import { api, APIError } from "./api"; +import { api } from "./api"; import { BootScreen, Shell } from "./components/Layout"; import { LoginPage } from "./pages/LoginPage"; +import { PermissionsProvider } from "./permissions"; import { Routes } from "./pages/Routes"; import { currentRoute, type RouteState } from "./routing"; +import type { AdminSession } from "./types"; export function App() { - const [actor, setActor] = useState(undefined); + // One GET /api/session at boot carries both the actor and the permission set the + // signed session was issued with. + const [session, setSession] = useState(undefined); const [route, setRoute] = useState(() => currentRoute()); useEffect(() => { @@ -17,14 +21,10 @@ export function App() { useEffect(() => { api.session() - .then((session) => setActor(session.actor)) - .catch((error) => { - if (error instanceof APIError && error.status === 401) { - setActor(null); - return; - } - setActor(null); - }); + .then((next) => setSession(next)) + // A 401 and an unreachable backend both end at the login screen; there is + // nothing the panel can render without a session. + .catch(() => setSession(null)); }, []); const navigate = (href: string) => { @@ -32,17 +32,19 @@ export function App() { setRoute(currentRoute()); }; - if (actor === undefined) { + if (session === undefined) { return ; } - if (actor === null) { - return ; + if (session === null) { + return ; } return ( - setActor(null)}> - - + + setSession(null)}> + + + ); } diff --git a/cmd/telesrv-admin/web/src/api.ts b/cmd/telesrv-admin/web/src/api.ts index d2475c47..59e58c50 100644 --- a/cmd/telesrv-admin/web/src/api.ts +++ b/cmd/telesrv-admin/web/src/api.ts @@ -1,11 +1,23 @@ import type { AccountDetail, AccountListResponse, + AccountRatingDetail, + AccountRatingListResponse, + AdminLoginResult, + AdminSession, BotDetail, BotListResponse, + BotVerificationCountsResponse, + BotVerifierListResponse, ChannelDetail, + CustomVerificationListResponse, + CustomVerificationRequestDetail, + CustomVerificationRequestListResponse, + VerificationIconListResponse, EmojiListResponse, ChannelListResponse, + CollectibleUsernameDetail, + CollectibleUsernameListResponse, CommandResult, GroupMessageDetail, GroupMessageListResponse, @@ -16,7 +28,10 @@ import type { ModerationReport, OfficialStarGiftListResponse, StarGiftCollectiblePreview, - StarGiftListResponse + StarGiftListResponse, + VerificationApplicationDetail, + VerificationApplicationListResponse, + VerificationCountsResponse } from "./types"; export class APIError extends Error { @@ -28,12 +43,81 @@ export class APIError extends Error { } } +// The backend publishes the CSRF token in a deliberately readable cookie and +// refuses every mutating request whose X-CSRF-Token header does not repeat it +// (cmd/telesrv-admin/security.go). Echoing it here — inside request — is what +// keeps a new endpoint from silently shipping without the header. +const csrfCookieName = "telesrv_admin_csrf"; +const csrfHeaderName = "X-CSRF-Token"; + +// Login answers with the token in the body as well as in Set-Cookie. Keeping the +// body value is the fallback for the window where the browser has not applied +// the cookie yet, or where the cookie is not readable back to the script. +let issuedCSRFToken = ""; + +export function rememberCSRFToken(token: string | undefined): void { + issuedCSRFToken = (token ?? "").trim(); +} + +function readCSRFCookie(): string { + if (typeof document === "undefined") return ""; + for (const chunk of document.cookie.split(";")) { + const entry = chunk.trim(); + const separator = entry.indexOf("="); + if (separator <= 0 || entry.slice(0, separator) !== csrfCookieName) continue; + try { + return decodeURIComponent(entry.slice(separator + 1)); + } catch { + return entry.slice(separator + 1); + } + } + return ""; +} + +// The cookie wins: it is the value the server compares against, and it survives +// a page reload that the in-memory copy does not. +export function csrfToken(): string { + return readCSRFCookie() || issuedCSRFToken; +} + +// Same classification the backend uses: GET/HEAD/OPTIONS are safe, everything +// else carries a token. +function mutatingMethod(method: string | undefined): boolean { + const verb = (method ?? "GET").toUpperCase(); + return verb !== "GET" && verb !== "HEAD" && verb !== "OPTIONS"; +} + +function plainHeaders(source: HeadersInit | undefined): Record { + if (!source) return {}; + if (source instanceof Headers) { + const out: Record = {}; + source.forEach((value, key) => { + out[key] = value; + }); + return out; + } + if (Array.isArray(source)) { + return Object.fromEntries(source); + } + return { ...source }; +} + async function request(url: string, init: RequestInit = {}): Promise { const isForm = typeof FormData !== "undefined" && init.body instanceof FormData; + // A multipart body must keep the boundary the browser generates, so its + // Content-Type is left alone; the CSRF header is added either way. + const headers: Record = isForm ? {} : { "Content-Type": "application/json" }; + Object.assign(headers, plainHeaders(init.headers)); + if (mutatingMethod(init.method)) { + const token = csrfToken(); + if (token) { + headers[csrfHeaderName] = token; + } + } const response = await fetch(url, { credentials: "same-origin", - headers: isForm ? init.headers : { "Content-Type": "application/json", ...(init.headers ?? {}) }, - ...init + ...init, + headers }); const text = await response.text(); const data = text ? JSON.parse(text) : null; @@ -52,11 +136,16 @@ export function errorMessage(error: unknown): string { } export const api = { - session: () => request<{ actor: string }>("/api/session"), - login: (secret: string) => request<{ actor: string }>("/api/login", { - method: "POST", - body: JSON.stringify({ secret }) - }), + session: () => request("/api/session"), + login: async (secret: string) => { + const result = await request("/api/login", { + method: "POST", + body: JSON.stringify({ secret }) + }); + // Stashed here rather than in the caller so no login path can forget it. + rememberCSRFToken(result.csrf_token); + return result; + }, logout: () => request<{ ok: boolean }>("/api/logout", { method: "POST", body: "{}" }), accounts: (params: URLSearchParams) => request(`/api/accounts?${params.toString()}`), account: (id: number) => request(`/api/accounts/${id}`), @@ -64,6 +153,36 @@ export const api = { channel: (id: number) => request(`/api/channels/${id}`), bots: (params: URLSearchParams) => request(`/api/bots?${params.toString()}`), bot: (id: number) => request(`/api/bots/${id}`), + collectibleUsernames: (params: URLSearchParams) => + request(`/api/collectible-usernames?${params.toString()}`), + collectibleUsername: (id: string) => + request(`/api/collectible-usernames/${encodeURIComponent(id)}`), + accountRatings: (params: URLSearchParams) => + request(`/api/account-ratings?${params.toString()}`), + accountRating: (userID: string) => + request(`/api/account-ratings/${encodeURIComponent(userID)}`), + verificationApplications: (params: URLSearchParams) => + request(`/api/verification/applications?${params.toString()}`), + // The application id is an int64 decimal string end to end, so it is never + // parsed into a number on the way to the URL. + verificationApplication: (id: string) => + request(`/api/verification/applications/${encodeURIComponent(id)}`), + verificationCounts: () => request("/api/verification/counts"), + // Third-party verification lives under its own prefix: the two mechanisms share + // no state, so they share no route either. + botVerifiers: (params: URLSearchParams) => + request(`/api/botverification/verifiers?${params.toString()}`), + verificationIcons: (params: URLSearchParams) => + request(`/api/botverification/icons?${params.toString()}`), + customVerifications: (params: URLSearchParams) => + request(`/api/botverification/marks?${params.toString()}`), + customVerificationRequests: (params: URLSearchParams) => + request(`/api/botverification/requests?${params.toString()}`), + // The application id is an int64 decimal string end to end, so it is never parsed + // into a number on the way to the URL. + customVerificationRequest: (id: string) => + request(`/api/botverification/requests/${encodeURIComponent(id)}`), + botVerificationCounts: () => request("/api/botverification/counts"), emoji: (params: URLSearchParams) => request(`/api/emoji?${params.toString()}`), emojiAnimation: (documentID: string) => request>(`/api/emoji/${encodeURIComponent(documentID)}/animation`), messages: (params: URLSearchParams) => request(`/api/messages?${params.toString()}`), diff --git a/cmd/telesrv-admin/web/src/components/ActionButton.tsx b/cmd/telesrv-admin/web/src/components/ActionButton.tsx index e2d41ad1..13926547 100644 --- a/cmd/telesrv-admin/web/src/components/ActionButton.tsx +++ b/cmd/telesrv-admin/web/src/components/ActionButton.tsx @@ -16,7 +16,9 @@ export function ActionButton({ icon, compact = false, tone = "danger", - onDone + disabled = false, + onDone, + onError }: { label: string; path: string; @@ -24,7 +26,15 @@ export function ActionButton({ icon?: ReactNode; compact?: boolean; tone?: ActionTone; + // disabled keeps a form from opening the confirm flow at all while its own + // validation is unhappy, so the operator fixes the field instead of reading a + // backend rejection. + disabled?: boolean; onDone?: () => void; + // onError lets a page react to a failure the operator cannot fix by editing the + // form — an optimistic-locking 409, say — and replace the raw backend text with + // an explanation by returning it. + onError?: (error: unknown) => string | undefined; }) { const { t } = useI18n(); const [open, setOpen] = useState(false); @@ -54,7 +64,7 @@ export function ActionButton({ onDone?.(); } } catch (err) { - setError(errorMessage(err)); + setError(onError?.(err) || errorMessage(err)); } finally { setBusy(false); } @@ -75,6 +85,7 @@ export function ActionButton({ + ) : null} + + {value ? ( +
+ +
+ {value.FirstName || "-"} + {value.ID} +
+ {displayUsername(value.Username) || "-"} +
+ ) : null} +
+ + setQuery(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + void search(); + } + }} + placeholder={t("picker.botPlaceholder")} + /> + +
+ {error &&
{error}
} +
+ {rows.map((row) => ( + + ))} + {rows.length === 0 && !busy ?
{t("common.noResults")}
: null} +
+ + ); +} + export function ChannelPicker({ label, value, diff --git a/cmd/telesrv-admin/web/src/components/Layout.tsx b/cmd/telesrv-admin/web/src/components/Layout.tsx index 4f86887c..9b982ea7 100644 --- a/cmd/telesrv-admin/web/src/components/Layout.tsx +++ b/cmd/telesrv-admin/web/src/components/Layout.tsx @@ -1,4 +1,6 @@ import { + AtSign, + BadgeCheck, Bot, ChevronDown, Database, @@ -10,6 +12,8 @@ import { ShieldAlert, ShieldCheck, Smile, + Stamp, + Trophy, Users, Gift, Send @@ -17,6 +21,7 @@ import { import { useEffect, useState, type ReactNode } from "react"; import { api } from "../api"; import { LanguageSwitch, useI18n } from "../i18n"; +import { permissionBotVerificationReview, permissionVerificationReview, useCan } from "../permissions"; import { type Navigate, type RouteState, routeSubtitle, routeTitle } from "../routing"; import { ThemeSwitch } from "../theme"; import { AppLink } from "./AppLink"; @@ -51,6 +56,12 @@ export function Shell({ children: ReactNode; }) { const { t } = useI18n(); + // The verification queue is hidden for a session without verification.review: + // the entry would only lead to a 403 (and the route itself is gated as well). + const canReviewVerification = useCan(permissionVerificationReview); + // Same reasoning for the third-party queue, which has its own right: the two + // sections are granted independently, so one entry can be visible without the other. + const canReviewBotVerification = useCan(permissionBotVerificationReview); const messagesActive = route.path.startsWith("/messages"); const [messagesOpen, setMessagesOpen] = useState(messagesActive); @@ -82,6 +93,14 @@ export function Shell({ } href="/channels" route={route} navigate={navigate}>{t("layout.channels")} } href="/bots" route={route} navigate={navigate}>{t("layout.bots")} } href="/moderation" route={route} navigate={navigate}>{t("layout.moderation")} + {canReviewVerification && ( + } href="/verification" route={route} navigate={navigate}>{t("layout.verification")} + )} + {canReviewBotVerification && ( + } href="/bot-verification" route={route} navigate={navigate}>{t("layout.botVerification")} + )} + } href="/collectible-usernames" route={route} navigate={navigate}>{t("layout.collectibleUsernames")} + } href="/account-ratings" route={route} navigate={navigate}>{t("layout.accountRatings")} } href="/gifts" route={route} navigate={navigate}>{t("layout.gifts")} } href="/give-gifts" route={route} navigate={navigate}>{t("layout.giveGifts")} } href="/emoji" route={route} navigate={navigate}>{t("layout.emoji")} diff --git a/cmd/telesrv-admin/web/src/components/ui.tsx b/cmd/telesrv-admin/web/src/components/ui.tsx index ad0247f5..b0bb95c0 100644 --- a/cmd/telesrv-admin/web/src/components/ui.tsx +++ b/cmd/telesrv-admin/web/src/components/ui.tsx @@ -1,8 +1,8 @@ import { CircleAlert } from "lucide-react"; import type { ReactNode } from "react"; import { useI18n } from "../i18n"; -import { formatDate } from "../lib/format"; -import type { AuditLogRow } from "../types"; +import { displayUsername, formatDate } from "../lib/format"; +import type { AccountUsername, AuditLogRow } from "../types"; type Tone = "neutral" | "good" | "danger" | "warn"; @@ -129,3 +129,33 @@ export function LoadingSurface({ label }: { label: string }) { export function JsonBlock({ value }: { value: string }) { return
{value || "{}"}
; } + +// UsernameCell renders a peer's editable username with its collectible usernames +// branching off underneath, in the order clients project them. +// +// An inactive collectible is shown rather than hidden: the peer still owns it, it +// just does not resolve publicly, and an operator looking for "where did that name +// go" needs to see it. It is marked instead of dropped. +// Pass an empty username to render the branch on its own, which is what the +// detail header does: it already shows the editable slot on the line above. +export function UsernameCell({ username, collectibles }: { username?: string; collectibles?: AccountUsername[] | null }) { + const { t } = useI18n(); + const main = displayUsername(username ?? ""); + const branch = collectibles ?? []; + if (branch.length === 0) { + return <>{main || "-"}; + } + return ( + <> + {main} +
    + {branch.map((item) => ( +
  • + {displayUsername(item.Username)} + {!item.Active && {t("usernames.inactive")}} +
  • + ))} +
+ + ); +} diff --git a/cmd/telesrv-admin/web/src/i18n.tsx b/cmd/telesrv-admin/web/src/i18n.tsx index a9b93984..38c1885b 100644 --- a/cmd/telesrv-admin/web/src/i18n.tsx +++ b/cmd/telesrv-admin/web/src/i18n.tsx @@ -591,7 +591,381 @@ const translations: Record> = { "audit.status": "Status", "audit.dryRun": "Dry-run", "audit.reason": "Reason", - "audit.time": "Time" + "audit.time": "Time", + "common.loadMore": "Load more", + "route.collectibleUsernames": "Collectible Usernames", + "route.collectibleUsernamesSubtitle": "Console / Collectible usernames", + "route.accountRatings": "Account Rating", + "route.accountRatingsSubtitle": "Console / Account rating", + "layout.collectibleUsernames": "NFT Usernames", + "layout.accountRatings": "Account Rating", + "usernames.pageTitle": "Collectible usernames", + "usernames.eyebrow": "NFT usernames / Registry", + "usernames.metricLoaded": "Loaded rows", + "usernames.metricVault": "In vault", + "usernames.metricOwned": "Held by owners", + "usernames.metricBurned": "Burned", + "usernames.mintTitle": "Mint a collectible username", + "usernames.mintHint": "Creates the asset together with its purchase record. Keep the owner as vault to mint it unassigned.", + "usernames.mint": "Mint username", + "usernames.mintNote": "Username, currency and amount are required; the dry-run checks availability first.", + "usernames.ownerKind": "Owner type", + "usernames.ownerVault": "Vault (no owner)", + "usernames.ownerUser": "User owner", + "usernames.ownerChannel": "Channel owner", + "usernames.currency": "Currency", + "usernames.amount": "Amount ({currency})", + "usernames.cryptoCurrency": "Crypto currency", + "usernames.cryptoNone": "None", + "usernames.cryptoAmount": "Crypto amount ({currency})", + "usernames.amountHint": "Amounts are typed in whole {currency} and stored as the smallest units the API and fragment.collectibleInfo carry, so clients render the price you meant. Up to {decimals} decimal places. Clients will show: {preview}.", + "usernames.amountInvalid": "That is not a valid {currency} amount: digits only, with at most {decimals} decimal places.", + "usernames.inactive": "inactive", + "usernames.url": "Marketplace URL", + "usernames.purchaseDate": "Purchase date (UTC)", + "usernames.purchaseTime": "Purchase time (UTC)", + "usernames.searchPlaceholder": "Search by username", + "usernames.statusAll": "All statuses", + "usernames.statusVault": "Vault", + "usernames.statusOwned": "Owned", + "usernames.statusBurned": "Burned", + "usernames.price": "Price", + "usernames.transfers": "Transfers", + "usernames.registryActive": "Active in profile", + "usernames.registryHidden": "Hidden in profile", + "usernames.loadingDetail": "Loading collectible username…", + "usernames.detailTitle": "Collectible {username}", + "usernames.detailEyebrow": "NFT usernames / Asset", + "usernames.assetID": "Asset #{id}", + "usernames.transferCount": "{count} transfers", + "usernames.originalOwner": "Original owner", + "usernames.openOwnerAccount": "Open owner account", + "usernames.openOwnerChannel": "Open owner channel", + "usernames.openMarketplace": "Open marketplace page", + "usernames.transferTitle": "Transfer ownership", + "usernames.transferHint": "Pick the recipient; the transfer is appended to the provenance history.", + "usernames.recipientKind": "Recipient type", + "usernames.recipientUser": "To user", + "usernames.recipientChannel": "To channel", + "usernames.transferNote": "The current owner loses the username immediately after confirmation.", + "usernames.transfer": "Transfer", + "usernames.historyTitle": "Provenance history", + "usernames.historyHint": "Mint, transfer, revoke and burn events in chronological order.", + "usernames.eventKind": "Event", + "usernames.fromPeer": "From", + "usernames.toPeer": "To", + "usernames.actionDock": "Asset operations", + "usernames.revoke": "Revoke to vault", + "usernames.revokeHint": "Takes the username away from its owner and returns it to the vault; it can be issued again later.", + "usernames.burn": "Burn permanently", + "usernames.burnHint": "Irreversible: the username is destroyed and can never be issued again.", + "usernames.burnedHint": "This username is burned — no further operations are possible.", + "usernames.delete": "Delete record", + "usernames.deleteHint": "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.", + "usernames.kind.mint": "Mint", + "usernames.kind.transfer": "Transfer", + "usernames.kind.revoke": "Revoke", + "usernames.kind.burn": "Burn", + "rating.pageTitle": "Account rating leaderboard", + "rating.eyebrow": "Rating / Leaderboard", + "rating.metricLoaded": "Loaded rows", + "rating.metricTopLevel": "Top level", + "rating.metricAvgLevel": "Average level", + "rating.metricPending": "With pending points", + "rating.searchPlaceholder": "Search by username, name or user ID", + "rating.minLevel": "Min level", + "rating.userID": "User ID", + "rating.level": "Level", + "rating.stars": "Points", + "rating.progress": "Progress to next level", + "rating.pending": "Pending", + "rating.computedAt": "Computed", + "rating.levelValue": "Level {level}", + "rating.maxLevel": "Max level reached", + "rating.progressHint": "{remaining} left to reach {target}", + "rating.loadingDetail": "Loading account rating…", + "rating.detailTitle": "Rating of {user}", + "rating.detailEyebrow": "Rating / Component breakdown", + "rating.pendingBadge": "Pending {amount}", + "rating.nextLevel": "Next level threshold", + "rating.toNextLevel": "Points to next level", + "rating.breakdownTitle": "How the rating adds up", + "rating.breakdownHint": "Contribution of every source: stars, activity, moderation penalties and manual corrections.", + "rating.breakdownMismatch": "Components add up to {sum} while the stored rating is {total}. Recompute to resolve the drift.", + "rating.breakdownPending": "Components already include {amount} that reaches the score only on the date below.", + "rating.currentLevelStars": "Current level threshold", + "rating.nextLevelStars": "Next level threshold", + "rating.pendingTitle": "Pending points", + "rating.pendingHint": "Already earned, but counted towards the rating only on the date below.", + "rating.pendingDate": "Applied on", + "rating.eventsTitle": "Rating events", + "rating.eventsHint": "Every rating change with its source, actor and reason.", + "rating.eventKind": "Source", + "rating.amount": "Change", + "rating.actionDock": "Rating operations", + "rating.openAccount": "Open account", + "rating.recompute": "Recompute", + "rating.recomputeHint": "Rebuilds the rating from stars, activity, penalties and manual corrections.", + "rating.adjustTitle": "Manual correction", + "rating.adjustAmount": "Value (negative allowed)", + "rating.adjust": "Apply correction", + "rating.adjustHint": "The value is added to the manual component; a negative number lowers the rating.", + "rating.componentStars": "Stars", + "rating.componentStarsHint": "Purchased and received stars", + "rating.componentActivity": "Activity", + "rating.componentActivityHint": "Messages, sessions and long-term engagement", + "rating.componentPenalty": "Penalties", + "rating.componentPenaltyHint": "Moderation decisions and restrictions", + "rating.componentManual": "Manual corrections", + "rating.componentManualHint": "Adjustments made by admins", + "rating.componentTotal": "Total rating", + "rating.kind.stars": "Stars", + "rating.kind.activity": "Activity", + "rating.kind.moderation": "Moderation", + "rating.kind.manual": "Manual", + "rating.kind.recompute": "Recompute", + "route.verification": "Official Verification", + "route.verificationSubtitle": "Console / Verification", + "layout.verification": "Verification", + "permission.deniedTitle": "Not enough rights", + "permission.deniedEyebrow": "Console / Access", + "permission.deniedBody": "This session was not granted the {permission} permission, so the section stays closed.", + "permission.deniedHeading": "Section unavailable", + "permission.deniedHint": "Ask an operator to add the permission to TELESRV_ADMIN_UI_PERMISSIONS and sign in again.", + "verification.pageTitle": "Verification queue", + "verification.eyebrow": "Verification / Queue", + "verification.searchPlaceholder": "Application id, peer id, username or title", + "verification.statusAll": "All statuses", + "verification.targetType": "Target type", + "verification.targetTypeAll": "All types", + "verification.reviewer": "Reviewer", + "verification.reviewerPlaceholder": "Any reviewer", + "verification.target": "Target", + "verification.applicant": "Applicant", + "verification.category": "Category", + "verification.submittedAt": "Submitted", + "verification.alreadyVerified": "Badge already on", + "verification.status.draft": "Draft", + "verification.status.submitted": "Submitted", + "verification.status.in_review": "In review", + "verification.status.approved": "Approved", + "verification.status.rejected": "Rejected", + "verification.status.cancelled": "Cancelled", + "verification.type.bot": "Bot", + "verification.type.channel": "Channel", + "verification.type.supergroup": "Supergroup", + "verification.type.user": "User", + "verification.loadingDetail": "Loading the application…", + "verification.detailTitle": "Application #{id}", + "verification.detailEyebrow": "Verification / Review", + "verification.conflict": "Another admin has already changed this application. The data has been reloaded — check the status before deciding again.", + "verification.controlsOk": "Control confirmed", + "verification.controlsLost": "No control over the target", + "verification.controlsOkHint": "The applicant controls the target right now — checked against the live records, not against the submission snapshot.", + "verification.controlsLostHint": "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.", + "verification.targetSection": "Target", + "verification.targetHint": "The peer the badge would be attached to, as it exists right now.", + "verification.openTarget": "Open target", + "verification.targetTitle": "Title", + "verification.targetID": "Peer ID", + "verification.applicantSection": "Applicant", + "verification.applicantHint": "Who filed the application and whether they still hold rights on the target.", + "verification.openApplicant": "Open account", + "verification.applicantID": "User ID", + "verification.applicationSection": "Application", + "verification.applicationHint": "Everything the applicant submitted, rendered as plain text.", + "verification.correlationID": "Correlation ID", + "verification.createdAt": "Created", + "verification.description": "Description", + "verification.officialWebsite": "Official website", + "verification.socialLinks": "Social links", + "verification.pressLinks": "Press coverage", + "verification.additionalNote": "Applicant comment", + "verification.notProvided": "Not provided", + "verification.linkSafetyHint": "Only http:// and https:// links are clickable and open in a new tab; anything else is shown as text.", + "verification.decisionSection": "Decision", + "verification.decisionHint": "What was decided, by whom, and with which wording.", + "verification.reviewedAt": "Decided", + "verification.version": "Version (optimistic lock)", + "verification.decisionReason": "Decision reason", + "verification.noDecision": "No decision yet", + "verification.internalNote": "Internal note", + "verification.adminOnly": "admins only", + "verification.eventsSection": "History", + "verification.eventsHint": "Immutable trail of every status transition, with actor and reason.", + "verification.eventKind": "Event", + "verification.transition": "From → to", + "verification.eventNote": "Internal note", + "verification.kind.created": "Created", + "verification.kind.updated": "Updated", + "verification.kind.submitted": "Submitted", + "verification.kind.claimed": "Claimed", + "verification.kind.approved": "Approved", + "verification.kind.rejected": "Rejected", + "verification.kind.cancelled": "Cancelled", + "verification.kind.revoked": "Badge revoked", + "verification.kind.notified": "Applicant notified", + "verification.actionDock": "Review actions", + "verification.noActions": "This status has no available actions.", + "verification.claim": "Take into review", + "verification.claimHint": "Assigns the application to you and moves it to in review, so two reviewers never work on the same one.", + "verification.internalNotePlaceholder": "Handover note for other reviewers", + "verification.internalNoteHint": "Optional. Stored with the decision and visible to admins only — never sent to the applicant.", + "verification.alreadyVerifiedHint": "The target already carries the badge; approving only records the decision.", + "verification.approve": "Approve", + "verification.approveHint": "Grants the official badge to the target and closes the application.", + "verification.reject": "Reject", + "verification.rejectHint": "The reason is mandatory: it is the wording the applicant is told, so write what exactly was missing.", + "verification.dangerZone": "Danger zone", + "verification.revoke": "Revoke verification", + "verification.revokeHint": "Clears the badge from the target. The approved application stays in history.", + "verification.revokeNotVerified": "The target carries no badge right now — there is nothing to revoke.", + "route.botVerification": "Third-party verification", + "route.botVerificationSubtitle": "Console / Third-party verification", + "layout.botVerification": "Third-party marks", + "picker.system": "System", + "picker.botPlaceholder": "Bot username or id", + "botverification.pageTitle": "Third-party verification", + "botverification.eyebrow": "Third-party verification / Verifiers, icons, marks", + "botverification.explainTitle": "A verifier company's icon — not the official checkmark", + "botverification.explainText": "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.", + "botverification.explainIcon": "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.", + "botverification.explainOfficial": "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.", + "botverification.openOfficial": "Official verification", + "botverification.manageMissing": "This session can read the section and decide applications, but not change verifiers or the icon catalogue — that needs the botverification.manage permission.", + "botverification.tabRequests": "Applications", + "botverification.tabVerifiers": "Verifiers", + "botverification.tabIcons": "Icon catalogue", + "botverification.tabMarks": "Granted marks", + "botverification.queueTitle": "Application queue", + "botverification.queueHint": "Applications filed with a verifier bot by the owner of the peer. The counters cover the whole queue, not the page below.", + "botverification.searchPlaceholder": "Application id, peer id, username or title", + "botverification.statusAll": "All statuses", + "botverification.status.pending": "Pending", + "botverification.status.approved": "Approved", + "botverification.status.rejected": "Rejected", + "botverification.status.revoked": "Mark revoked", + "botverification.peer.user": "Account", + "botverification.peer.channel": "Channel", + "botverification.peerType": "Peer type", + "botverification.peerTypeAll": "All types", + "botverification.verifier": "Verifier", + "botverification.verifierAll": "All verifiers", + "botverification.verifierID": "Verifier bot ID", + "botverification.applicant": "Applicant", + "botverification.applicantID": "User ID", + "botverification.target": "Peer", + "botverification.targetTitle": "Title", + "botverification.targetID": "Peer ID", + "botverification.reason": "Stated reason", + "botverification.requestedDescription": "Requested description", + "botverification.description": "Description", + "botverification.createdAt": "Filed", + "botverification.company": "Company", + "botverification.companyPlaceholder": "Acme Verification Ltd", + "botverification.bot": "Bot", + "botverification.icon": "Icon", + "botverification.iconDocument": "Document ID", + "botverification.iconName": "Name", + "botverification.markCount": "Marks", + "botverification.grantedBy": "Granted by", + "botverification.notProvided": "Not set", + "botverification.verifiersTitle": "Verifier bots", + "botverification.verifiersHint": "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.", + "botverification.grantTitle": "Grant verifier status", + "botverification.updateTitle": "Update verifier", + "botverification.grantHint": "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.", + "botverification.grantBot": "Bot", + "botverification.grantIcon": "Icon from the catalogue", + "botverification.grantIconPick": "Pick an icon", + "botverification.defaultDescription": "Default description", + "botverification.defaultDescriptionPlaceholder": "Verified by Acme", + "botverification.canModify": "The verifier may replace the description per peer", + "botverification.canModifyShort": "Own description", + "botverification.canModifyHint": "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.", + "botverification.noActiveIcons": "The catalogue has no active icon, so there is nothing to grant. Add one in the icon catalogue first.", + "botverification.grantNote": "The bot can mark peers as soon as the row exists and is enabled.", + "botverification.grant": "Grant verifier status", + "botverification.update": "Update verifier", + "botverification.editing": "Updating {bot} — version {version} is sent as the optimistic lock, so a row somebody else changed meanwhile is refused instead of overwritten.", + "botverification.cancelEdit": "Cancel update", + "botverification.edit": "Edit", + "botverification.enable": "Enable", + "botverification.disable": "Disable", + "botverification.enabled": "Enabled", + "botverification.disabled": "disabled", + "botverification.disableHint": "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.", + "botverification.revokeVerifier": "Revoke status", + "botverification.revokeVerifierHint": "Revoking verifier status removes the row and every mark this verifier granted — the icon disappears from all of its peers at once.", + "botverification.iconsTitle": "Icon catalogue", + "botverification.iconsHint": "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.", + "botverification.addIconTitle": "Add or rename an icon", + "botverification.addIconHint": "The document id has to name a real custom emoji document on this deployment; the Emoji section lists them with their ids. Adding an id that already exists renames it instead of duplicating it.", + "botverification.iconNamePlaceholder": "Acme blue tick", + "botverification.iconOwner": "Owner", + "botverification.iconOwnerShared": "Shared", + "botverification.iconOwnerHint": "A shared icon may be granted to any verifier; picking an owner reserves it for that one bot.", + "botverification.iconDocumentHint": "A document id that resolves to nothing produces an invisible badge: the peer is marked in the database and the client draws nothing.", + "botverification.addIconNote": "Adding an icon grants nothing by itself — it only makes the document available to grant.", + "botverification.addIcon": "Save icon", + "botverification.iconActive": "Active", + "botverification.iconInactive": "Retired", + "botverification.usedBy": "Verifiers using it", + "botverification.activateIcon": "Activate", + "botverification.deactivateIcon": "Retire", + "botverification.deactivateIconHint": "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.", + "botverification.marksTitle": "Granted marks", + "botverification.marksHint": "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.", + "botverification.markSearchPlaceholder": "Peer id, username or title", + "botverification.revokeMark": "Remove mark", + "botverification.revokeMarkHint": "Removing a mark clears the icon and the description from the peer. The application it came from keeps its history.", + "botverification.loadingDetail": "Loading the application…", + "botverification.detailTitle": "Application #{id}", + "botverification.detailEyebrow": "Third-party verification / Review", + "botverification.conflict": "Another admin has already changed this application. The data has been reloaded — check the status before deciding again.", + "botverification.markActive": "Mark is live", + "botverification.markInactive": "No mark on the peer", + "botverification.markActiveHint": "This peer already carries this verifier's mark; approving refreshes the description and records the decision.", + "botverification.verifierSection": "Verifier", + "botverification.verifierHint": "The company whose icon the peer would carry, as its row stands right now.", + "botverification.openVerifier": "Open verifier bot", + "botverification.verifierMissing": "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.", + "botverification.verifierDisabledHint": "This verifier is disabled. It cannot mark anything new until an operator enables it again.", + "botverification.targetSection": "Peer", + "botverification.targetHint": "The account, bot or channel the icon would be attached to.", + "botverification.openTarget": "Open peer", + "botverification.applicantSection": "Applicant", + "botverification.applicantHint": "Who filed the application with the verifier bot.", + "botverification.openApplicant": "Open account", + "botverification.requestSection": "Application", + "botverification.requestHint": "What the applicant wrote, rendered as plain text.", + "botverification.correlationID": "Correlation ID", + "botverification.markPreview": "Description the mark would carry", + "botverification.markPreviewHint": "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.", + "botverification.descriptionIgnoredHint": "This verifier may not set a per-peer description, so the requested wording is ignored and the default is applied.", + "botverification.decisionSection": "Decision", + "botverification.decisionHint": "What was decided, by whom, and with which wording.", + "botverification.decidedBy": "Decided by", + "botverification.approvedAt": "Approved", + "botverification.rejectedAt": "Rejected", + "botverification.version": "Version (optimistic lock)", + "botverification.decisionReason": "Decision reason", + "botverification.noDecision": "No decision yet", + "botverification.internalNote": "Internal note", + "botverification.adminOnly": "admins only", + "botverification.internalNotePlaceholder": "Handover note for other admins", + "botverification.internalNoteHint": "Optional. Stored with the decision and visible to admins only — never sent to the applicant.", + "botverification.actionDock": "Decision", + "botverification.noActions": "This status has no available actions.", + "botverification.approve": "Approve", + "botverification.approveHint": "Puts the verifier's icon before the peer's name and its description in the profile, and messages the applicant.", + "botverification.reject": "Reject", + "botverification.rejectHint": "The reason is mandatory: it is the wording the applicant is told, so write what exactly was missing.", + "botverification.dangerZone": "Danger zone", + "botverification.revokeRequest": "Revoke mark", + "botverification.revokeRequestHint": "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.", + "botverification.revokeNoMark": "The peer carries no mark right now — revoking only closes the application.", + "botverification.rosterDenied": "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." }, zh: { "app.adminConsole": "管理控制台", @@ -1177,7 +1551,381 @@ const translations: Record> = { "audit.status": "状态", "audit.dryRun": "预演", "audit.reason": "原因", - "audit.time": "时间" + "audit.time": "时间", + "common.loadMore": "加载更多", + "route.collectibleUsernames": "收藏用户名", + "route.collectibleUsernamesSubtitle": "控制台 / 收藏用户名", + "route.accountRatings": "账号评级", + "route.accountRatingsSubtitle": "控制台 / 账号评级", + "layout.collectibleUsernames": "NFT 用户名", + "layout.accountRatings": "账号评级", + "usernames.pageTitle": "收藏用户名", + "usernames.eyebrow": "NFT 用户名 / 资产台账", + "usernames.metricLoaded": "已加载", + "usernames.metricVault": "库存中", + "usernames.metricOwned": "已归属", + "usernames.metricBurned": "已销毁", + "usernames.mintTitle": "铸造收藏用户名", + "usernames.mintHint": "同时写入购买记录;保持“库存”即不指定归属。", + "usernames.mint": "铸造用户名", + "usernames.mintNote": "用户名、币种与金额必填;预演会先校验可用性。", + "usernames.ownerKind": "归属类型", + "usernames.ownerVault": "库存(无归属)", + "usernames.ownerUser": "归属用户", + "usernames.ownerChannel": "归属频道", + "usernames.currency": "币种", + "usernames.amount": "金额({currency})", + "usernames.cryptoCurrency": "加密币种", + "usernames.cryptoNone": "无", + "usernames.cryptoAmount": "加密金额({currency})", + "usernames.amountHint": "金额按完整的 {currency} 输入,保存时会转换为 API 与 fragment.collectibleInfo 使用的最小单位,客户端才会显示你想要的价格。最多 {decimals} 位小数。客户端将显示:{preview}。", + "usernames.amountInvalid": "这不是有效的 {currency} 金额:只能是数字,最多 {decimals} 位小数。", + "usernames.inactive": "未启用", + "usernames.url": "市场链接", + "usernames.purchaseDate": "购买日期(UTC)", + "usernames.purchaseTime": "购买时间(UTC)", + "usernames.searchPlaceholder": "按用户名搜索", + "usernames.statusAll": "全部状态", + "usernames.statusVault": "库存", + "usernames.statusOwned": "已归属", + "usernames.statusBurned": "已销毁", + "usernames.price": "价格", + "usernames.transfers": "转移次数", + "usernames.registryActive": "资料中展示", + "usernames.registryHidden": "资料中隐藏", + "usernames.loadingDetail": "正在加载收藏用户名…", + "usernames.detailTitle": "收藏用户名 {username}", + "usernames.detailEyebrow": "NFT 用户名 / 资产详情", + "usernames.assetID": "资产 #{id}", + "usernames.transferCount": "转移 {count} 次", + "usernames.originalOwner": "最初归属", + "usernames.openOwnerAccount": "打开归属账号", + "usernames.openOwnerChannel": "打开归属频道", + "usernames.openMarketplace": "打开市场页面", + "usernames.transferTitle": "转移归属", + "usernames.transferHint": "选择接收方;转移会记入流转历史。", + "usernames.recipientKind": "接收方类型", + "usernames.recipientUser": "转给用户", + "usernames.recipientChannel": "转给频道", + "usernames.transferNote": "确认后当前持有者立即失去该用户名。", + "usernames.transfer": "转移", + "usernames.historyTitle": "流转历史", + "usernames.historyHint": "按时间顶序展示铸造、转移、收回与销毁事件。", + "usernames.eventKind": "事件", + "usernames.fromPeer": "来自", + "usernames.toPeer": "转至", + "usernames.actionDock": "资产操作", + "usernames.revoke": "收回至库存", + "usernames.revokeHint": "从持有者收回用户名并放回库存,之后可再次发放。", + "usernames.burn": "永久销毁", + "usernames.burnHint": "不可撤销:用户名将被销毁且永不可再发放。", + "usernames.burnedHint": "该用户名已销毁,无法再执行任何操作。", + "usernames.delete": "删除记录", + "usernames.deleteHint": "彻底删除该藏品及其持有历史,并释放用户名以便重新发放。适用于误发的用户名;销毁则会保留历史。", + "usernames.kind.mint": "铸造", + "usernames.kind.transfer": "转移", + "usernames.kind.revoke": "收回", + "usernames.kind.burn": "销毁", + "rating.pageTitle": "账号评级榜", + "rating.eyebrow": "评级 / 排行榜", + "rating.metricLoaded": "已加载", + "rating.metricTopLevel": "最高等级", + "rating.metricAvgLevel": "平均等级", + "rating.metricPending": "有待生效分值", + "rating.searchPlaceholder": "按用户名、姓名或用户 ID 搜索", + "rating.minLevel": "最低等级", + "rating.userID": "用户 ID", + "rating.level": "等级", + "rating.stars": "分值", + "rating.progress": "升级进度", + "rating.pending": "待生效", + "rating.computedAt": "计算时间", + "rating.levelValue": "{level} 级", + "rating.maxLevel": "已达最高等级", + "rating.progressHint": "距 {target} 还差 {remaining}", + "rating.loadingDetail": "正在加载账号评级…", + "rating.detailTitle": "{user} 的评级", + "rating.detailEyebrow": "评级 / 构成明细", + "rating.pendingBadge": "待生效 {amount}", + "rating.nextLevel": "下一级门槛", + "rating.toNextLevel": "升级所需分值", + "rating.breakdownTitle": "评级构成", + "rating.breakdownHint": "当前分值的来源:星星、活跃度、审核处罚与人工修正。", + "rating.breakdownMismatch": "各项合计为 {sum},而存储分值为 {total};请重新计算以消除偏差。", + "rating.breakdownPending": "各项已包含 {amount},但要到下列日期才计入分值。", + "rating.currentLevelStars": "当前等级门槛", + "rating.nextLevelStars": "下一等级门槛", + "rating.pendingTitle": "待生效分值", + "rating.pendingHint": "已获得但要到下列日期才计入评级的分值。", + "rating.pendingDate": "生效日期", + "rating.eventsTitle": "评级事件", + "rating.eventsHint": "每一次分值变动及其来源、操作者与原因。", + "rating.eventKind": "来源", + "rating.amount": "变动值", + "rating.actionDock": "评级操作", + "rating.openAccount": "打开账号", + "rating.recompute": "重新计算", + "rating.recomputeHint": "根据星星、活跃度、处罚与人工修正重新生成分值。", + "rating.adjustTitle": "人工修正", + "rating.adjustAmount": "数值(可为负)", + "rating.adjust": "应用修正", + "rating.adjustHint": "该数值会累加到人工修正项;填负数即为扣减。", + "rating.componentStars": "星星", + "rating.componentStarsHint": "购买与收到的星星", + "rating.componentActivity": "活跃度", + "rating.componentActivityHint": "消息、会话与长期活跃", + "rating.componentPenalty": "处罚", + "rating.componentPenaltyHint": "审核处置与限制", + "rating.componentManual": "人工修正", + "rating.componentManualHint": "管理员手动调整", + "rating.componentTotal": "总分", + "rating.kind.stars": "星星", + "rating.kind.activity": "活跃度", + "rating.kind.moderation": "审核", + "rating.kind.manual": "人工", + "rating.kind.recompute": "重新计算", + "route.verification": "官方认证", + "route.verificationSubtitle": "控制台 / 官方认证", + "layout.verification": "官方认证", + "permission.deniedTitle": "权限不足", + "permission.deniedEyebrow": "控制台 / 访问控制", + "permission.deniedBody": "当前会话没有 {permission} 权限,该板块保持关闭。", + "permission.deniedHeading": "板块不可用", + "permission.deniedHint": "请让运维在 TELESRV_ADMIN_UI_PERMISSIONS 中补上该权限,然后重新登录。", + "verification.pageTitle": "认证申请队列", + "verification.eyebrow": "认证 / 队列", + "verification.searchPlaceholder": "申请 ID、对象 ID、用户名或名称", + "verification.statusAll": "全部状态", + "verification.targetType": "对象类型", + "verification.targetTypeAll": "全部类型", + "verification.reviewer": "审核人", + "verification.reviewerPlaceholder": "全部审核人", + "verification.target": "认证对象", + "verification.applicant": "申请人", + "verification.category": "类别", + "verification.submittedAt": "提交时间", + "verification.alreadyVerified": "已有认证标记", + "verification.status.draft": "草稿", + "verification.status.submitted": "已提交", + "verification.status.in_review": "审核中", + "verification.status.approved": "已通过", + "verification.status.rejected": "已驳回", + "verification.status.cancelled": "已取消", + "verification.type.bot": "机器人", + "verification.type.channel": "频道", + "verification.type.supergroup": "超级群", + "verification.type.user": "用户", + "verification.loadingDetail": "正在加载申请…", + "verification.detailTitle": "申请 #{id}", + "verification.detailEyebrow": "认证 / 审核", + "verification.conflict": "该申请已被其他管理员修改,数据已重新加载;请确认状态后再次提交决定。", + "verification.controlsOk": "对象权限已确认", + "verification.controlsLost": "已失去对象权限", + "verification.controlsOkHint": "按当前实时记录核对(而非提交时的快照),申请人此刻仍然掌控该对象。", + "verification.controlsLostHint": "申请人已不再掌控该对象。此时通过,等于把认证标记发给并非持有人的一方,通常应当驳回。", + "verification.targetSection": "认证对象", + "verification.targetHint": "将要挂上认证标记的对象,展示的是当前实时状态。", + "verification.openTarget": "打开对象", + "verification.targetTitle": "名称", + "verification.targetID": "对象 ID", + "verification.applicantSection": "申请人", + "verification.applicantHint": "谁提交了申请,以及他是否仍持有该对象的权限。", + "verification.openApplicant": "打开账号", + "verification.applicantID": "用户 ID", + "verification.applicationSection": "申请内容", + "verification.applicationHint": "申请人填写的全部内容,一律按纯文本展示。", + "verification.correlationID": "关联 ID", + "verification.createdAt": "创建时间", + "verification.description": "说明", + "verification.officialWebsite": "官方网站", + "verification.socialLinks": "社交账号", + "verification.pressLinks": "媒体报道", + "verification.additionalNote": "申请人备注", + "verification.notProvided": "未填写", + "verification.linkSafetyHint": "只有 http:// 与 https:// 链接可点击并在新标签页打开,其余一律按文本显示。", + "verification.decisionSection": "决定", + "verification.decisionHint": "已记录的结论、审核人与理由。", + "verification.reviewedAt": "决定时间", + "verification.version": "版本(乐观锁)", + "verification.decisionReason": "决定理由", + "verification.noDecision": "尚无决定", + "verification.internalNote": "内部备注", + "verification.adminOnly": "仅管理员可见", + "verification.eventsSection": "历史记录", + "verification.eventsHint": "不可篡改的状态流转记录,含操作者与理由。", + "verification.eventKind": "事件", + "verification.transition": "状态变化", + "verification.eventNote": "内部备注", + "verification.kind.created": "创建", + "verification.kind.updated": "修改", + "verification.kind.submitted": "提交", + "verification.kind.claimed": "领取", + "verification.kind.approved": "通过", + "verification.kind.rejected": "驳回", + "verification.kind.cancelled": "取消", + "verification.kind.revoked": "撤销标记", + "verification.kind.notified": "已通知申请人", + "verification.actionDock": "审核操作", + "verification.noActions": "当前状态没有可执行的操作。", + "verification.claim": "领取审核", + "verification.claimHint": "把申请分配给自己并转入审核中,避免两名审核人同时处理同一条。", + "verification.internalNotePlaceholder": "给其他审核人的交接说明", + "verification.internalNoteHint": "可选。随决定一起保存,仅管理员可见,不会发送给申请人。", + "verification.alreadyVerifiedHint": "该对象已带有认证标记,通过操作只是补记这次决定。", + "verification.approve": "通过认证", + "verification.approveHint": "为该对象授予官方认证标记并结束申请。", + "verification.reject": "驳回", + "verification.rejectHint": "必须填写理由:这段文字会告知申请人,请写清究竟缺少什么。", + "verification.dangerZone": "高危操作", + "verification.revoke": "撤销认证", + "verification.revokeHint": "清除该对象的认证标记;已通过的申请仍作为历史保留。", + "verification.revokeNotVerified": "该对象当前没有认证标记,无需撤销。", + "route.botVerification": "第三方认证", + "route.botVerificationSubtitle": "控制台 / 第三方认证", + "layout.botVerification": "第三方标记", + "picker.system": "系统", + "picker.botPlaceholder": "机器人用户名或 ID", + "botverification.pageTitle": "第三方认证", + "botverification.eyebrow": "第三方认证 / 认证方、图标、标记", + "botverification.explainTitle": "这是认证公司的图标,不是官方认证标记", + "botverification.explainText": "第三方标记是认证机器人自己的图标,显示在账号、机器人或频道名称的“前面”,并在资料页附一行说明。它只表示“该认证方为此对象背书”,仅此而已。", + "botverification.explainIcon": "图标是一个自定义表情文档。客户端通过 messages.getCustomEmojiDocuments 拉取,所以指向不存在文档的 ID 会显示为完全没有标记——因此标记只能从下面的图标目录中授予,而不是手输一个数字。", + "botverification.explainOfficial": "官方认证标记是另一套机制,由平台在“官方认证”板块授予。两者分别存储、分别显示、分别撤销,任何一方都不代表另一方。", + "botverification.openOfficial": "官方认证", + "botverification.manageMissing": "本会话可以查看本板块并裁决申请,但不能修改认证方或图标目录——那需要 botverification.manage 权限。", + "botverification.tabRequests": "申请", + "botverification.tabVerifiers": "认证方", + "botverification.tabIcons": "图标目录", + "botverification.tabMarks": "已授予的标记", + "botverification.queueTitle": "申请队列", + "botverification.queueHint": "对象持有者向认证机器人提交的申请。计数覆盖整个队列,而不是下面这一页。", + "botverification.searchPlaceholder": "申请 ID、对象 ID、用户名或标题", + "botverification.statusAll": "全部状态", + "botverification.status.pending": "待处理", + "botverification.status.approved": "已通过", + "botverification.status.rejected": "已拒绝", + "botverification.status.revoked": "标记已撤销", + "botverification.peer.user": "账号", + "botverification.peer.channel": "频道", + "botverification.peerType": "对象类型", + "botverification.peerTypeAll": "全部类型", + "botverification.verifier": "认证方", + "botverification.verifierAll": "全部认证方", + "botverification.verifierID": "认证机器人 ID", + "botverification.applicant": "申请人", + "botverification.applicantID": "用户 ID", + "botverification.target": "对象", + "botverification.targetTitle": "标题", + "botverification.targetID": "对象 ID", + "botverification.reason": "申请理由", + "botverification.requestedDescription": "申请的说明文字", + "botverification.description": "说明", + "botverification.createdAt": "提交时间", + "botverification.company": "公司", + "botverification.companyPlaceholder": "Acme Verification Ltd", + "botverification.bot": "机器人", + "botverification.icon": "图标", + "botverification.iconDocument": "文档 ID", + "botverification.iconName": "名称", + "botverification.markCount": "标记数", + "botverification.grantedBy": "授予人", + "botverification.notProvided": "未设置", + "botverification.verifiersTitle": "认证机器人", + "botverification.verifiersHint": "获准发放自有标记的机器人。认证方身份按部署授予,所以这里的每一行都是运营人员手动打开的“标记发放机”。", + "botverification.grantTitle": "授予认证方身份", + "botverification.updateTitle": "更新认证方", + "botverification.grantHint": "机器人会拿到目录中的一个图标和一个用于背书的公司名。同一个接口也用于更新已有认证方,因此需要带上版本号。", + "botverification.grantBot": "机器人", + "botverification.grantIcon": "目录中的图标", + "botverification.grantIconPick": "选择图标", + "botverification.defaultDescription": "默认说明", + "botverification.defaultDescriptionPlaceholder": "由 Acme 认证", + "botverification.canModify": "允许认证方为每个对象单独改写说明", + "botverification.canModifyShort": "自定义说明", + "botverification.canModifyHint": "对应 botVerifierSettings.can_modify_custom_description:关闭时,该认证方发放的每个标记都使用上面的默认说明,无论申请人写了什么。", + "botverification.noActiveIcons": "目录中没有启用的图标,无法授予。请先在图标目录中添加。", + "botverification.grantNote": "记录存在且处于启用状态后,该机器人即可开始标记对象。", + "botverification.grant": "授予认证方身份", + "botverification.update": "更新认证方", + "botverification.editing": "正在更新 {bot} —— 版本 {version} 作为乐观锁一起提交,若该行已被他人改动则请求被拒绝,而不是被覆盖。", + "botverification.cancelEdit": "取消更新", + "botverification.edit": "编辑", + "botverification.enable": "启用", + "botverification.disable": "停用", + "botverification.enabled": "已启用", + "botverification.disabled": "已停用", + "botverification.disableHint": "停用是针对单个认证方的紧急开关:已发放的标记继续显示,但该机器人不能再标记新对象,其设置也不再投射到 botInfo。", + "botverification.revokeVerifier": "撤销身份", + "botverification.revokeVerifierHint": "撤销认证方身份会删除该行以及它发放过的所有标记——图标会同时从它的全部对象上消失。", + "botverification.iconsTitle": "图标目录", + "botverification.iconsHint": "认证方可用于标记的自定义表情文档。除此之外的任何东西都不能作为图标,所以错误的标记在这里被拦下,而不是事后修补。", + "botverification.addIconTitle": "添加或重命名图标", + "botverification.addIconHint": "文档 ID 必须对应本部署上真实存在的自定义表情文档;“表情”板块会列出它们及其 ID。添加一个已存在的 ID 会重命名它,而不是新建一条。", + "botverification.iconNamePlaceholder": "Acme 蓝标", + "botverification.iconOwner": "归属", + "botverification.iconOwnerShared": "共享", + "botverification.iconOwnerHint": "共享图标可以授予任何认证方;指定归属后则只保留给那一个机器人。", + "botverification.iconDocumentHint": "指向不存在文档的 ID 会产生“隐形标记”:数据库里对象已被标记,而客户端什么也画不出来。", + "botverification.addIconNote": "添加图标本身不授予任何东西,只是让该文档可被授予。", + "botverification.addIcon": "保存图标", + "botverification.iconActive": "启用", + "botverification.iconInactive": "已下架", + "botverification.usedBy": "使用中的认证方", + "botverification.activateIcon": "启用", + "botverification.deactivateIcon": "下架", + "botverification.deactivateIconHint": "下架后该图标不能再授予给新的认证方。已经带着它的标记不受影响:图标在授予时就复制到了标记上。", + "botverification.marksTitle": "已授予的标记", + "botverification.marksHint": "当前带有第三方标记的所有对象,无论由谁授予——运营裁决、认证机器人自己,或对象持有者通过 bots.setCustomVerification。", + "botverification.markSearchPlaceholder": "对象 ID、用户名或标题", + "botverification.revokeMark": "移除标记", + "botverification.revokeMarkHint": "移除标记会清除对象上的图标和说明。对应的申请仍保留历史记录。", + "botverification.loadingDetail": "正在加载申请…", + "botverification.detailTitle": "申请 #{id}", + "botverification.detailEyebrow": "第三方认证 / 审核", + "botverification.conflict": "另一位管理员已经改动过这份申请。数据已重新加载——请先确认状态再做决定。", + "botverification.markActive": "标记生效中", + "botverification.markInactive": "对象上没有标记", + "botverification.markActiveHint": "该对象已带有此认证方的标记;通过申请只会刷新说明并记录这次决定。", + "botverification.verifierSection": "认证方", + "botverification.verifierHint": "对象将要佩戴其图标的公司,按其当前记录显示。", + "botverification.openVerifier": "打开认证机器人", + "botverification.verifierMissing": "认证方记录已不存在:这份申请提交后其身份被撤销了。没有可授予的图标,因此这份申请只能被拒绝。", + "botverification.verifierDisabledHint": "该认证方已被停用,在运营人员重新启用之前不能标记任何新对象。", + "botverification.targetSection": "对象", + "botverification.targetHint": "图标将要附加到的账号、机器人或频道。", + "botverification.openTarget": "打开对象", + "botverification.applicantSection": "申请人", + "botverification.applicantHint": "向认证机器人提交这份申请的人。", + "botverification.openApplicant": "打开账号", + "botverification.requestSection": "申请内容", + "botverification.requestHint": "申请人填写的内容,按纯文本呈现。", + "botverification.correlationID": "关联 ID", + "botverification.markPreview": "标记将显示的说明", + "botverification.markPreviewHint": "与后端的解析规则一致:只有当该认证方被允许自定义说明时才使用申请人的文字,否则使用认证方的默认说明。", + "botverification.descriptionIgnoredHint": "该认证方不能为单个对象设置说明,因此申请的文字被忽略,改用默认说明。", + "botverification.decisionSection": "决定", + "botverification.decisionHint": "谁做了什么决定,以及用了什么措辞。", + "botverification.decidedBy": "裁决人", + "botverification.approvedAt": "通过时间", + "botverification.rejectedAt": "拒绝时间", + "botverification.version": "版本(乐观锁)", + "botverification.decisionReason": "决定理由", + "botverification.noDecision": "尚未裁决", + "botverification.internalNote": "内部备注", + "botverification.adminOnly": "仅管理员可见", + "botverification.internalNotePlaceholder": "留给其他管理员的交接说明", + "botverification.internalNoteHint": "可选。与决定一起保存,仅管理员可见——绝不会发给申请人。", + "botverification.actionDock": "裁决", + "botverification.noActions": "当前状态没有可执行的操作。", + "botverification.approve": "通过", + "botverification.approveHint": "把认证方的图标放到对象名称前面,把说明放进资料页,并通知申请人。", + "botverification.reject": "拒绝", + "botverification.rejectHint": "理由必填:申请人看到的就是这段文字,所以请写清到底缺了什么。", + "botverification.dangerZone": "危险操作", + "botverification.revokeRequest": "撤销标记", + "botverification.revokeRequestHint": "从对象上移除图标和说明,并把申请关闭为“已撤销”。对象若持有官方认证标记,不受影响。", + "botverification.revokeNoMark": "该对象当前没有标记——撤销只会关闭这份申请。", + "botverification.rosterDenied": "服务器拒绝了本会话读取认证方名单与图标目录(403),因此这两个列表为空——申请仍然可以审核。" }, ru: { "app.adminConsole": "Панель администратора", @@ -1763,7 +2511,381 @@ const translations: Record> = { "audit.status": "Статус", "audit.dryRun": "Тестовый запуск", "audit.reason": "Причина", - "audit.time": "Время" + "audit.time": "Время", + "common.loadMore": "Показать ещё", + "route.collectibleUsernames": "Коллекционные юзернеймы", + "route.collectibleUsernamesSubtitle": "Консоль / Коллекционные юзернеймы", + "route.accountRatings": "Рейтинг аккаунтов", + "route.accountRatingsSubtitle": "Консоль / Рейтинг аккаунтов", + "layout.collectibleUsernames": "NFT-юзернеймы", + "layout.accountRatings": "Рейтинг аккаунтов", + "usernames.pageTitle": "Коллекционные юзернеймы", + "usernames.eyebrow": "NFT-юзернеймы / Реестр", + "usernames.metricLoaded": "Загружено строк", + "usernames.metricVault": "В хранилище", + "usernames.metricOwned": "У владельцев", + "usernames.metricBurned": "Сожжено", + "usernames.mintTitle": "Выпустить юзернейм", + "usernames.mintHint": "Создаёт актив вместе с записью о покупке. Оставьте «хранилище», чтобы выпустить юзернейм без владельца.", + "usernames.mint": "Выпустить юзернейм", + "usernames.mintNote": "Юзернейм, валюта и сумма обязательны — тестовый запуск сначала проверит, свободен ли юзернейм.", + "usernames.ownerKind": "Тип владельца", + "usernames.ownerVault": "Хранилище (без владельца)", + "usernames.ownerUser": "Владелец-пользователь", + "usernames.ownerChannel": "Владелец-канал", + "usernames.currency": "Валюта", + "usernames.amount": "Сумма ({currency})", + "usernames.cryptoCurrency": "Криптовалюта", + "usernames.cryptoNone": "Нет", + "usernames.cryptoAmount": "Сумма в крипте ({currency})", + "usernames.amountHint": "Сумма вводится в целых {currency}, а хранится в наименьших единицах, которые принимают API и fragment.collectibleInfo, — тогда клиент покажет именно ту цену, которую вы задали. До {decimals} знаков после запятой. Клиент покажет: {preview}.", + "usernames.amountInvalid": "Это не похоже на сумму в {currency}: только цифры и не больше {decimals} знаков после запятой.", + "usernames.inactive": "выключен", + "usernames.url": "Ссылка на маркетплейс", + "usernames.purchaseDate": "Дата покупки (UTC)", + "usernames.purchaseTime": "Время покупки (UTC)", + "usernames.searchPlaceholder": "Поиск по юзернейму", + "usernames.statusAll": "Все статусы", + "usernames.statusVault": "В хранилище", + "usernames.statusOwned": "У владельца", + "usernames.statusBurned": "Сожжён", + "usernames.price": "Цена", + "usernames.transfers": "Передачи", + "usernames.registryActive": "Активен в профиле", + "usernames.registryHidden": "Скрыт в профиле", + "usernames.loadingDetail": "Загружаем коллекционный юзернейм…", + "usernames.detailTitle": "Юзернейм {username}", + "usernames.detailEyebrow": "NFT-юзернеймы / Актив", + "usernames.assetID": "Актив №{id}", + "usernames.transferCount": "Передач: {count}", + "usernames.originalOwner": "Первый владелец", + "usernames.openOwnerAccount": "Открыть аккаунт владельца", + "usernames.openOwnerChannel": "Открыть канал владельца", + "usernames.openMarketplace": "Открыть страницу на маркетплейсе", + "usernames.transferTitle": "Передать юзернейм", + "usernames.transferHint": "Выберите получателя — передача попадёт в историю владения.", + "usernames.recipientKind": "Тип получателя", + "usernames.recipientUser": "Пользователю", + "usernames.recipientChannel": "Каналу", + "usernames.transferNote": "После подтверждения текущий владелец сразу теряет юзернейм.", + "usernames.transfer": "Передать", + "usernames.historyTitle": "История владения", + "usernames.historyHint": "Выпуск, передачи, отзывы и сжигание — в хронологическом порядке.", + "usernames.eventKind": "Событие", + "usernames.fromPeer": "От", + "usernames.toPeer": "Кому", + "usernames.actionDock": "Операции с активом", + "usernames.revoke": "Отозвать в хранилище", + "usernames.revokeHint": "Забирает юзернейм у владельца и возвращает в хранилище — позже его можно выдать снова.", + "usernames.burn": "Сжечь безвозвратно", + "usernames.burnHint": "Необратимо: юзернейм уничтожается и больше никогда не будет выдан.", + "usernames.burnedHint": "Юзернейм сожжён — операции с ним больше недоступны.", + "usernames.delete": "Удалить запись", + "usernames.deleteHint": "Стирает актив вместе с историей владения и полностью освобождает юзернейм для нового выпуска. Это для случая «выпустил не то имя»; сжигание, наоборот, историю сохраняет.", + "usernames.kind.mint": "Выпуск", + "usernames.kind.transfer": "Передача", + "usernames.kind.revoke": "Отзыв", + "usernames.kind.burn": "Сжигание", + "rating.pageTitle": "Рейтинг аккаунтов", + "rating.eyebrow": "Рейтинг / Лидерборд", + "rating.metricLoaded": "Загружено строк", + "rating.metricTopLevel": "Максимальный уровень", + "rating.metricAvgLevel": "Средний уровень", + "rating.metricPending": "С отложенными баллами", + "rating.searchPlaceholder": "Поиск по юзернейму, имени или ID", + "rating.minLevel": "Мин. уровень", + "rating.userID": "ID пользователя", + "rating.level": "Уровень", + "rating.stars": "Баллы", + "rating.progress": "Прогресс до следующего уровня", + "rating.pending": "Отложено", + "rating.computedAt": "Пересчитан", + "rating.levelValue": "Уровень {level}", + "rating.maxLevel": "Максимальный уровень", + "rating.progressHint": "До {target} осталось {remaining}", + "rating.loadingDetail": "Загружаем рейтинг аккаунта…", + "rating.detailTitle": "Рейтинг {user}", + "rating.detailEyebrow": "Рейтинг / Разбор по компонентам", + "rating.pendingBadge": "Отложено {amount}", + "rating.nextLevel": "Порог следующего уровня", + "rating.toNextLevel": "Баллов до следующего уровня", + "rating.breakdownTitle": "Из чего сложился рейтинг", + "rating.breakdownHint": "Вклад каждого источника: звёзды, активность, штрафы модерации и ручные корректировки.", + "rating.breakdownMismatch": "Сумма компонентов — {sum}, а сохранённый рейтинг — {total}. Запустите пересчёт, чтобы устранить расхождение.", + "rating.breakdownPending": "Компоненты уже учитывают {amount}, которые войдут в рейтинг только в указанную дату.", + "rating.currentLevelStars": "Порог текущего уровня", + "rating.nextLevelStars": "Порог следующего уровня", + "rating.pendingTitle": "Отложенные баллы", + "rating.pendingHint": "Баллы уже начислены, но войдут в рейтинг только в указанную дату.", + "rating.pendingDate": "Дата применения", + "rating.eventsTitle": "События рейтинга", + "rating.eventsHint": "Каждое изменение рейтинга с источником, исполнителем и причиной.", + "rating.eventKind": "Источник", + "rating.amount": "Изменение", + "rating.actionDock": "Операции с рейтингом", + "rating.openAccount": "Открыть аккаунт", + "rating.recompute": "Пересчитать", + "rating.recomputeHint": "Собирает рейтинг заново из звёзд, активности, штрафов и ручных корректировок.", + "rating.adjustTitle": "Ручная корректировка", + "rating.adjustAmount": "Значение (можно отрицательное)", + "rating.adjust": "Скорректировать", + "rating.adjustHint": "Значение прибавляется к ручной составляющей; отрицательное число уменьшает рейтинг.", + "rating.componentStars": "Звёзды", + "rating.componentStarsHint": "Купленные и полученные звёзды", + "rating.componentActivity": "Активность", + "rating.componentActivityHint": "Сообщения, сессии и долгосрочная вовлечённость", + "rating.componentPenalty": "Штрафы", + "rating.componentPenaltyHint": "Решения модерации и ограничения", + "rating.componentManual": "Ручные корректировки", + "rating.componentManualHint": "Правки, внесённые администраторами", + "rating.componentTotal": "Итоговый рейтинг", + "rating.kind.stars": "Звёзды", + "rating.kind.activity": "Активность", + "rating.kind.moderation": "Модерация", + "rating.kind.manual": "Вручную", + "rating.kind.recompute": "Пересчёт", + "route.verification": "Официальная верификация", + "route.verificationSubtitle": "Консоль / Верификация", + "layout.verification": "Верификация", + "permission.deniedTitle": "Недостаточно прав", + "permission.deniedEyebrow": "Консоль / Доступ", + "permission.deniedBody": "Этой сессии не выдано право {permission}, поэтому раздел закрыт.", + "permission.deniedHeading": "Раздел недоступен", + "permission.deniedHint": "Попросите добавить право в TELESRV_ADMIN_UI_PERMISSIONS и войдите заново.", + "verification.pageTitle": "Очередь заявок на верификацию", + "verification.eyebrow": "Верификация / Очередь", + "verification.searchPlaceholder": "ID заявки, ID цели, юзернейм или название", + "verification.statusAll": "Все статусы", + "verification.targetType": "Тип цели", + "verification.targetTypeAll": "Все типы", + "verification.reviewer": "Ревьюер", + "verification.reviewerPlaceholder": "Любой ревьюер", + "verification.target": "Цель", + "verification.applicant": "Заявитель", + "verification.category": "Категория", + "verification.submittedAt": "Подана", + "verification.alreadyVerified": "Бейдж уже стоит", + "verification.status.draft": "Черновик", + "verification.status.submitted": "Подана", + "verification.status.in_review": "На рассмотрении", + "verification.status.approved": "Одобрена", + "verification.status.rejected": "Отклонена", + "verification.status.cancelled": "Отменена", + "verification.type.bot": "Бот", + "verification.type.channel": "Канал", + "verification.type.supergroup": "Супергруппа", + "verification.type.user": "Пользователь", + "verification.loadingDetail": "Загружаем заявку…", + "verification.detailTitle": "Заявка №{id}", + "verification.detailEyebrow": "Верификация / Разбор заявки", + "verification.conflict": "Заявку уже изменил другой администратор. Данные перезагружены — проверьте статус и примите решение заново.", + "verification.controlsOk": "Права на цель подтверждены", + "verification.controlsLost": "Прав на цель больше нет", + "verification.controlsOkHint": "Заявитель управляет целью прямо сейчас — проверено по актуальным записям, а не по снимку на момент подачи.", + "verification.controlsLostHint": "Заявитель больше не управляет целью. Одобрить — значит выдать бейдж тому, кто уже не владеет пиром; обычно это причина отказать.", + "verification.targetSection": "Цель", + "verification.targetHint": "Пир, которому достанется бейдж, — в том виде, в каком он существует сейчас.", + "verification.openTarget": "Открыть цель", + "verification.targetTitle": "Название", + "verification.targetID": "ID пира", + "verification.applicantSection": "Заявитель", + "verification.applicantHint": "Кто подал заявку и сохранились ли у него права на цель.", + "verification.openApplicant": "Открыть аккаунт", + "verification.applicantID": "ID пользователя", + "verification.applicationSection": "Заявка", + "verification.applicationHint": "Всё, что заявитель прислал сам; выводится как обычный текст.", + "verification.correlationID": "Correlation ID", + "verification.createdAt": "Создана", + "verification.description": "Описание", + "verification.officialWebsite": "Официальный сайт", + "verification.socialLinks": "Соцсети", + "verification.pressLinks": "Публикации в СМИ", + "verification.additionalNote": "Комментарий заявителя", + "verification.notProvided": "Не указано", + "verification.linkSafetyHint": "Кликабельны только ссылки на http:// и https://, и открываются они в новой вкладке; всё остальное показано текстом.", + "verification.decisionSection": "Решение", + "verification.decisionHint": "Что решили, кто решил и с какой формулировкой.", + "verification.reviewedAt": "Решение принято", + "verification.version": "Версия (оптимистичная блокировка)", + "verification.decisionReason": "Причина решения", + "verification.noDecision": "Решения пока нет", + "verification.internalNote": "Внутренняя заметка", + "verification.adminOnly": "видно только администраторам", + "verification.eventsSection": "История", + "verification.eventsHint": "Неизменяемый след всех переходов статуса — с автором и причиной.", + "verification.eventKind": "Событие", + "verification.transition": "Было → стало", + "verification.eventNote": "Внутренняя заметка", + "verification.kind.created": "Создана", + "verification.kind.updated": "Изменена", + "verification.kind.submitted": "Подана", + "verification.kind.claimed": "Взята в работу", + "verification.kind.approved": "Одобрена", + "verification.kind.rejected": "Отклонена", + "verification.kind.cancelled": "Отменена", + "verification.kind.revoked": "Бейдж снят", + "verification.kind.notified": "Заявитель уведомлён", + "verification.actionDock": "Действия по заявке", + "verification.noActions": "В этом статусе действий нет.", + "verification.claim": "Взять в работу", + "verification.claimHint": "Закрепляет заявку за вами и переводит её в статус «на рассмотрении», чтобы двое не разбирали одно и то же.", + "verification.internalNotePlaceholder": "Заметка для других ревьюеров", + "verification.internalNoteHint": "Необязательно. Сохраняется вместе с решением и видно только администраторам — заявителю не уходит.", + "verification.alreadyVerifiedHint": "Бейдж на цели уже стоит: одобрение лишь зафиксирует решение.", + "verification.approve": "Одобрить", + "verification.approveHint": "Выдаёт цели официальный бейдж и закрывает заявку.", + "verification.reject": "Отклонить", + "verification.rejectHint": "Причина обязательна: именно эту формулировку увидит заявитель, поэтому напишите, чего не хватило.", + "verification.dangerZone": "Опасная зона", + "verification.revoke": "Снять верификацию", + "verification.revokeHint": "Убирает бейдж с цели. Одобренная заявка остаётся в истории.", + "verification.revokeNotVerified": "Бейджа на цели сейчас нет — снимать нечего.", + "route.botVerification": "Сторонняя верификация", + "route.botVerificationSubtitle": "Консоль / Сторонняя верификация", + "layout.botVerification": "Сторонние метки", + "picker.system": "Системный", + "picker.botPlaceholder": "Юзернейм бота или ID", + "botverification.pageTitle": "Сторонняя верификация", + "botverification.eyebrow": "Сторонняя верификация / Верификаторы, иконки, метки", + "botverification.explainTitle": "Это иконка компании-верификатора, а не официальная галочка", + "botverification.explainText": "Сторонняя метка — это собственная иконка бота-верификатора, которая рисуется ПЕРЕД именем аккаунта, бота или канала, плюс одна строка описания в профиле. Она значит только одно: «этот верификатор поручился за этот аккаунт».", + "botverification.explainIcon": "Иконка — это документ кастомного эмодзи. Клиент забирает его через messages.getCustomEmojiDocuments, поэтому ID, за которым нет реального документа, выглядит как полное отсутствие метки. Именно поэтому метки выдаются из каталога ниже, а не из набранного руками числа.", + "botverification.explainOfficial": "Официальная галочка — другой механизм, её выдаёт платформа в разделе «Официальная верификация». Они хранятся, показываются и снимаются по отдельности, и одна не подразумевает другую.", + "botverification.openOfficial": "Официальная верификация", + "botverification.manageMissing": "Эта сессия может читать раздел и решать заявки, но не может менять верификаторов и каталог иконок — для этого нужно право botverification.manage.", + "botverification.tabRequests": "Заявки", + "botverification.tabVerifiers": "Верификаторы", + "botverification.tabIcons": "Каталог иконок", + "botverification.tabMarks": "Выданные метки", + "botverification.queueTitle": "Очередь заявок", + "botverification.queueHint": "Заявки, поданные боту-верификатору владельцем аккаунта или канала. Счётчики считают всю очередь, а не страницу ниже.", + "botverification.searchPlaceholder": "ID заявки, ID цели, юзернейм или название", + "botverification.statusAll": "Все статусы", + "botverification.status.pending": "Ожидает решения", + "botverification.status.approved": "Одобрена", + "botverification.status.rejected": "Отклонена", + "botverification.status.revoked": "Метка снята", + "botverification.peer.user": "Аккаунт", + "botverification.peer.channel": "Канал", + "botverification.peerType": "Тип цели", + "botverification.peerTypeAll": "Все типы", + "botverification.verifier": "Верификатор", + "botverification.verifierAll": "Все верификаторы", + "botverification.verifierID": "ID бота-верификатора", + "botverification.applicant": "Заявитель", + "botverification.applicantID": "ID пользователя", + "botverification.target": "Цель", + "botverification.targetTitle": "Название", + "botverification.targetID": "ID цели", + "botverification.reason": "Обоснование", + "botverification.requestedDescription": "Запрошенное описание", + "botverification.description": "Описание", + "botverification.createdAt": "Подана", + "botverification.company": "Компания", + "botverification.companyPlaceholder": "ООО «Ромашка Верификация»", + "botverification.bot": "Бот", + "botverification.icon": "Иконка", + "botverification.iconDocument": "ID документа", + "botverification.iconName": "Название", + "botverification.markCount": "Метки", + "botverification.grantedBy": "Кто выдал", + "botverification.notProvided": "Не задано", + "botverification.verifiersTitle": "Боты-верификаторы", + "botverification.verifiersHint": "Боты, которым разрешено выдавать свою метку. Статус верификатора выдаётся вручную на уровне сервера, так что каждая строка здесь — это печатный станок бейджей, включённый оператором.", + "botverification.grantTitle": "Выдать статус верификатора", + "botverification.updateTitle": "Обновить верификатора", + "botverification.grantHint": "Бот получает иконку из каталога и название компании, от имени которой он поручается. Тот же вызов обновляет существующего верификатора — поэтому в нём есть version.", + "botverification.grantBot": "Бот", + "botverification.grantIcon": "Иконка из каталога", + "botverification.grantIconPick": "Выберите иконку", + "botverification.defaultDescription": "Описание по умолчанию", + "botverification.defaultDescriptionPlaceholder": "Проверено компанией «Ромашка»", + "botverification.canModify": "Верификатор может задавать своё описание для каждой цели", + "botverification.canModifyShort": "Своё описание", + "botverification.canModifyHint": "Это botVerifierSettings.can_modify_custom_description: если выключено, любая метка этого верификатора несёт описание по умолчанию — что бы ни просил заявитель.", + "botverification.noActiveIcons": "В каталоге нет активных иконок, выдавать нечего. Сначала добавьте иконку в каталог.", + "botverification.grantNote": "Бот сможет ставить метки сразу, как только строка появится и будет включена.", + "botverification.grant": "Выдать статус", + "botverification.update": "Обновить верификатора", + "botverification.editing": "Обновляем {bot} — версия {version} уходит как оптимистичная блокировка: если строку успел изменить кто-то другой, запрос отклонят, а не перезапишут.", + "botverification.cancelEdit": "Отменить обновление", + "botverification.edit": "Изменить", + "botverification.enable": "Включить", + "botverification.disable": "Отключить", + "botverification.enabled": "Включён", + "botverification.disabled": "отключён", + "botverification.disableHint": "Отключение — это рубильник для одного верификатора: уже выданные метки продолжают показываться, но новых бот поставить не может, и его настройки перестают уезжать в botInfo.", + "botverification.revokeVerifier": "Отозвать статус", + "botverification.revokeVerifierHint": "Отзыв статуса удаляет строку и все метки, которые этот верификатор выдал: иконка исчезнет сразу у всех его целей.", + "botverification.iconsTitle": "Каталог иконок", + "botverification.iconsHint": "Документы кастомных эмодзи, которыми верификатор может помечать цели. Ничего другого иконкой быть не может, поэтому неверный бейдж отсекается здесь, а не исправляется потом.", + "botverification.addIconTitle": "Добавить иконку или переименовать", + "botverification.addIconHint": "ID документа должен указывать на реальный документ кастомного эмодзи на этом сервере; раздел «Эмодзи» показывает их вместе с ID. Повторное добавление того же ID переименует запись, а не создаст вторую.", + "botverification.iconNamePlaceholder": "Синяя галочка «Ромашки»", + "botverification.iconOwner": "Владелец", + "botverification.iconOwnerShared": "Общая", + "botverification.iconOwnerHint": "Общую иконку можно выдать любому верификатору; если указать владельца, она закрепится только за этим ботом.", + "botverification.iconDocumentHint": "ID, за которым нет документа, даёт невидимый бейдж: в базе цель помечена, а клиент не рисует ничего.", + "botverification.addIconNote": "Добавление иконки само по себе ничего не выдаёт — оно лишь делает документ доступным для выдачи.", + "botverification.addIcon": "Сохранить иконку", + "botverification.iconActive": "Активна", + "botverification.iconInactive": "Выведена", + "botverification.usedBy": "Используют верификаторов", + "botverification.activateIcon": "Активировать", + "botverification.deactivateIcon": "Вывести", + "botverification.deactivateIconHint": "Выведенную иконку больше нельзя выдать новым верификаторам. У уже выданных меток она остаётся: иконка копируется в метку в момент выдачи.", + "botverification.marksTitle": "Выданные метки", + "botverification.marksHint": "Все цели, которые сейчас несут стороннюю метку, кем бы она ни была выдана: решением оператора, самим ботом-верификатором или владельцем цели через bots.setCustomVerification.", + "botverification.markSearchPlaceholder": "ID цели, юзернейм или название", + "botverification.revokeMark": "Снять метку", + "botverification.revokeMarkHint": "Снятие метки убирает с цели иконку и описание. Заявка, из которой метка появилась, остаётся в истории.", + "botverification.loadingDetail": "Загружаем заявку…", + "botverification.detailTitle": "Заявка #{id}", + "botverification.detailEyebrow": "Сторонняя верификация / Разбор", + "botverification.conflict": "Заявку уже изменил другой админ. Данные перезагружены — посмотрите статус перед новым решением.", + "botverification.markActive": "Метка стоит", + "botverification.markInactive": "Метки на цели нет", + "botverification.markActiveHint": "На этой цели метка этого верификатора уже стоит; одобрение обновит описание и зафиксирует решение.", + "botverification.verifierSection": "Верификатор", + "botverification.verifierHint": "Компания, чью иконку получит цель, — в том виде, в каком её строка выглядит сейчас.", + "botverification.openVerifier": "Открыть бота-верификатора", + "botverification.verifierMissing": "Строки верификатора больше нет: его статус отозвали уже после подачи заявки. Выдавать нечего, поэтому заявку остаётся только отклонить.", + "botverification.verifierDisabledHint": "Верификатор отключён. Пока оператор не включит его снова, новые метки он ставить не может.", + "botverification.targetSection": "Цель", + "botverification.targetHint": "Аккаунт, бот или канал, к которому будет прикреплена иконка.", + "botverification.openTarget": "Открыть цель", + "botverification.applicantSection": "Заявитель", + "botverification.applicantHint": "Кто подал заявку боту-верификатору.", + "botverification.openApplicant": "Открыть аккаунт", + "botverification.requestSection": "Заявка", + "botverification.requestHint": "Что написал заявитель — как обычный текст.", + "botverification.correlationID": "Correlation ID", + "botverification.markPreview": "Описание, которое получит метка", + "botverification.markPreviewHint": "Считается так же, как на бэкенде: формулировка заявителя берётся только если верификатору разрешено своё описание, иначе применяется описание верификатора по умолчанию.", + "botverification.descriptionIgnoredHint": "Этому верификатору нельзя задавать описание для отдельной цели, поэтому запрошенный текст игнорируется и применяется описание по умолчанию.", + "botverification.decisionSection": "Решение", + "botverification.decisionHint": "Кто и что решил и какими словами.", + "botverification.decidedBy": "Решение принял", + "botverification.approvedAt": "Одобрена", + "botverification.rejectedAt": "Отклонена", + "botverification.version": "Версия (оптимистичная блокировка)", + "botverification.decisionReason": "Причина решения", + "botverification.noDecision": "Решения пока нет", + "botverification.internalNote": "Внутренняя заметка", + "botverification.adminOnly": "только для админов", + "botverification.internalNotePlaceholder": "Заметка для других админов", + "botverification.internalNoteHint": "Необязательно. Хранится вместе с решением и видна только админам — заявителю не отправляется.", + "botverification.actionDock": "Решение по заявке", + "botverification.noActions": "В этом статусе действий нет.", + "botverification.approve": "Одобрить", + "botverification.approveHint": "Ставит иконку верификатора перед именем цели, описание — в профиль, и уведомляет заявителя.", + "botverification.reject": "Отклонить", + "botverification.rejectHint": "Причина обязательна: именно эту формулировку увидит заявитель, поэтому напишите, чего именно не хватило.", + "botverification.dangerZone": "Опасная зона", + "botverification.revokeRequest": "Снять метку", + "botverification.revokeRequestHint": "Убирает с цели иконку и описание и закрывает заявку как «метка снята». Официальная галочка, если она есть, не затрагивается.", + "botverification.revokeNoMark": "Метки на цели сейчас нет — снятие только закроет заявку.", + "botverification.rosterDenied": "Сервер не отдал этой сессии список верификаторов и каталог иконок (403), поэтому оба списка здесь пустые — заявки разбирать всё равно можно." } }; diff --git a/cmd/telesrv-admin/web/src/lib/format.ts b/cmd/telesrv-admin/web/src/lib/format.ts index 31bad425..c18eefca 100644 --- a/cmd/telesrv-admin/web/src/lib/format.ts +++ b/cmd/telesrv-admin/web/src/lib/format.ts @@ -44,12 +44,131 @@ export function formatUnix(value: number): string { return date.toLocaleString(); } +// safeHttpURL vets a link an applicant typed. Only http(s) is turned into an +// anchor: a submitted string may just as well be javascript:, data: or a bare +// word, and must stay inert text in that case. The parsed href is returned so a +// malformed authority cannot slip through the prefix test. +export function safeHttpURL(value: string): string { + const raw = (value ?? "").trim(); + if (!/^https?:\/\//i.test(raw)) return ""; + try { + const parsed = new URL(raw); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return ""; + return parsed.href; + } catch { + return ""; + } +} + export function toInt(value: string): number { if (!value.trim()) return 0; const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) ? parsed : 0; } +// int64 values arrive as JSON strings; keep parsing tolerant so an unexpected +// empty string or "null" never renders as NaN. +export function toNumeric(value: string): number { + const raw = (value ?? "").trim(); + if (!raw) return 0; + const parsed = Number(raw); + return Number.isFinite(parsed) ? parsed : 0; +} + +export function formatQuantity(value: string): string { + const raw = (value ?? "").trim(); + if (!raw) return "0"; + const parsed = Number(raw); + return Number.isFinite(parsed) ? parsed.toLocaleString() : raw; +} + +// Currency scaling for fragment.collectibleInfo. +// +// The wire format is integer smallest units: core.telegram.org says amount is +// "Total price in the smallest units of the currency (integer, not +// float/double)" -- $1.45 is 145 -- and crypto_amount likewise, so TON is +// nanotons (1 TON = 1e9). Clients divide by that exponent before drawing the +// price, which is why a panel that both stores and shows the raw integer makes an +// operator type 900 for "900 TON" and Telegram Desktop then renders 0.0000009. +// +// Everything the operator reads or types in the panel is therefore in whole +// currency units, and these helpers are the only conversion boundary. +const currencyExponents: Record = { + // Stars have no subunit: an XTR amount is a count of stars. + XTR: 0, + // Nanotons. + TON: 9, + // Fiat minor units. + USD: 2, + EUR: 2, + RUB: 2 +}; + +export function currencyExponent(currency: string): number { + const key = (currency ?? "").trim().toUpperCase(); + // Two decimals is the ISO 4217 default, and it is what an unknown fiat code + // most likely is; guessing 0 would silently multiply a price by 100. + return key in currencyExponents ? currencyExponents[key] : 2; +} + +// formatCurrencyAmount renders smallest units as whole currency units. It works +// on the decimal string rather than a JS number so a nanoton amount beyond +// Number.MAX_SAFE_INTEGER is not rounded on the way to the screen. +export function formatCurrencyAmount(value: string, currency: string): string { + const raw = (value ?? "").trim(); + if (!raw) return "0"; + if (!/^-?\d+$/.test(raw)) return raw; + const exponent = currencyExponent(currency); + const negative = raw.startsWith("-"); + const digits = (negative ? raw.slice(1) : raw).replace(/^0+(?=\d)/, ""); + const padded = digits.padStart(exponent + 1, "0"); + const whole = padded.slice(0, padded.length - exponent) || "0"; + let fraction = exponent > 0 ? padded.slice(padded.length - exponent) : ""; + // Fiat keeps its two decimals the way a client draws them ($10.00); a + // nine-decimal crypto amount would just be a wall of zeros, so trim those. + if (exponent > 2) fraction = fraction.replace(/0+$/, ""); + const sign = negative ? "-" : ""; + return fraction ? `${sign}${groupDigits(whole)}.${fraction}` : `${sign}${groupDigits(whole)}`; +} + +// groupDigits inserts thousands separators without going through a JS number, so +// a value past Number.MAX_SAFE_INTEGER keeps every digit. +function groupDigits(digits: string): string { + return digits.replace(/\B(?=(\d{3})+(?!\d))/g, " "); +} + +// formatCurrency is formatCurrencyAmount with the code appended, which is the +// shape every price cell in the panel wants. +export function formatCurrency(value: string, currency: string): string { + const code = (currency ?? "").trim().toUpperCase(); + const amount = formatCurrencyAmount(value, code); + return code ? `${amount} ${code}` : amount; +} + +// toSmallestUnits turns what the operator typed -- whole currency units, with an +// optional fraction -- into the integer decimal string the API expects. It +// returns null for anything that is not a plain non-negative amount, or that +// carries more decimals than the currency has, so the form can refuse instead of +// silently truncating a price. +export function toSmallestUnits(value: string, currency: string): string | null { + const raw = (value ?? "").trim().replace(/\s+/g, "").replace(",", "."); + if (!raw) return "0"; + if (!/^\d*(\.\d*)?$/.test(raw) || raw === "." ) return null; + const exponent = currencyExponent(currency); + const [wholePart, fractionPart = ""] = raw.split("."); + if (fractionPart.length > exponent) return null; + const digits = `${wholePart || "0"}${fractionPart.padEnd(exponent, "0")}`.replace(/^0+(?=\d)/, ""); + return digits === "" ? "0" : digits; +} + +export function formatSigned(value: string): string { + const raw = (value ?? "").trim(); + if (!raw) return "0"; + const parsed = Number(raw); + if (!Number.isFinite(parsed)) return raw; + return parsed > 0 ? `+${parsed.toLocaleString()}` : parsed.toLocaleString(); +} + export function parseIDs(value: string, invalidMessage = "msg ids invalid"): number[] { const ids = value .split(/[\s,]+/) diff --git a/cmd/telesrv-admin/web/src/pages/AccountDetailPage.tsx b/cmd/telesrv-admin/web/src/pages/AccountDetailPage.tsx index 3d02b80f..26a67175 100644 --- a/cmd/telesrv-admin/web/src/pages/AccountDetailPage.tsx +++ b/cmd/telesrv-admin/web/src/pages/AccountDetailPage.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; import { ActionButton } from "../components/ActionButton"; import { AuthorizationTable } from "../components/AuthorizationTable"; -import { Alert, AuditTable, Badge, LoadingSurface, PageFrame, SectionHead, SplitLayout, Summary } from "../components/ui"; +import { Alert, AuditTable, Badge, LoadingSurface, PageFrame, SectionHead, SplitLayout, Summary, UsernameCell } from "../components/ui"; import { ScamFakeActions, ScamFakeBadges } from "../components/flags"; import { ColorAction, EmojiStatusAction, SupportAction, UsernameAction } from "../components/attributes"; import { useI18n } from "../i18n"; @@ -65,6 +65,11 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
{displayName(account)}
{displayUsername(account.Username) || t("account.noUsername")} · {displayPhone(account.Phone) || t("account.noPhone")}
+ {account.Collectibles?.length > 0 && ( +
+ +
+ )}
{account.PremiumUntil > 0 ? {t("account.premium")} : {t("account.notPremium")}} diff --git a/cmd/telesrv-admin/web/src/pages/AccountRatingDetailPage.tsx b/cmd/telesrv-admin/web/src/pages/AccountRatingDetailPage.tsx new file mode 100644 index 00000000..7f1e7826 --- /dev/null +++ b/cmd/telesrv-admin/web/src/pages/AccountRatingDetailPage.tsx @@ -0,0 +1,261 @@ +import { ArrowLeft, Calculator, RefreshCw, SlidersHorizontal, User } from "lucide-react"; +import { useEffect, useState } from "react"; +import { api, errorMessage } from "../api"; +import { ActionButton } from "../components/ActionButton"; +import { Alert, Badge, EmptyRow, LoadingSurface, Metric, PageFrame, SectionHead, SplitLayout, Summary } from "../components/ui"; +import { useI18n } from "../i18n"; +import { displayUsername, formatDate, formatQuantity, formatSigned, toNumeric } from "../lib/format"; +import type { Navigate } from "../routing"; +import type { AccountRatingDetail, AccountRatingEventKind, AccountRatingRow } from "../types"; +import { LevelBadge, RatingProgress, levelProgress } from "./AccountRatingsPage"; + +export function AccountRatingDetailPage({ userID, navigate }: { userID: string; navigate: Navigate }) { + const { t } = useI18n(); + const [detail, setDetail] = useState(null); + const [error, setError] = useState(""); + const [busy, setBusy] = useState(false); + const [adjustment, setAdjustment] = useState(""); + + async function load() { + setBusy(true); + setError(""); + try { + setDetail(await api.accountRating(userID)); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + useEffect(() => { + void load(); + }, [userID]); + + if (error && !detail) { + return {error}; + } + if (!detail) { + return ; + } + + const rating = detail.rating; + const events = detail.events ?? []; + const pending = toNumeric(rating.PendingStars); + const progress = levelProgress(rating); + // user_id / amount are `,string` int64 fields on the backend, so they stay + // decimal strings and never pass through a float. + const payloadUserID = rating.UserID || userID; + + return ( + + + + + } + > + {error && {error}} + +
+
+
{displayUsername(rating.Username) || rating.FirstName || t("bots.unnamed")}
+
{t("rating.userID")}: {rating.UserID}
+
+
+ + {pending !== 0 && {t("rating.pendingBadge", { amount: formatSigned(rating.PendingStars) })}} +
+
+ +
+ + + + = 80 ? "good" : "neutral"} + /> +
+ +
+ + +
+ + + + +
+
+ +
+
+ + {pending !== 0 && ( +
+ +
+ + +
+
+ )} + +
+ +
+ + + + + + + + + + + + + {events.map((row) => ( + + + + + + + + + ))} + {events.length === 0 && } + +
{t("common.id")}{t("rating.eventKind")}{t("rating.amount")}{t("audit.reason")}{t("audit.actor")}{t("common.time")}
{row.ID}{formatSigned(row.Amount)}{row.Reason || "-"}{row.Actor || "-"}{formatDate(row.CreatedAt) || "-"}
+
+
+
+ } + side={ +
+
{t("rating.actionDock")}
+ +
+ } + tone="neutral" + path="/api/actions/recompute-account-rating" + payload={() => ({ user_id: payloadUserID })} + onDone={load} + /> +
+

{t("rating.recomputeHint")}

+
{t("rating.adjustTitle")}
+ +
+ } + tone="warn" + path="/api/actions/adjust-account-rating" + payload={() => ({ + user_id: payloadUserID, + amount: String(Number.parseInt(adjustment.trim() || "0", 10) || 0) + })} + onDone={() => { + setAdjustment(""); + void load(); + }} + /> +
+

{t("rating.adjustHint")}

+
+ } + /> + + ); +} + +function Breakdown({ rating }: { rating: AccountRatingRow }) { + const { t } = useI18n(); + // PenaltyComponent is stored as a positive magnitude and subtracted by the + // scorer, so it is shown (and summed) as a negative contribution. + const components = [ + { key: "stars", label: t("rating.componentStars"), hint: t("rating.componentStarsHint"), value: toNumeric(rating.StarsComponent) }, + { key: "activity", label: t("rating.componentActivity"), hint: t("rating.componentActivityHint"), value: toNumeric(rating.ActivityComponent) }, + { key: "penalty", label: t("rating.componentPenalty"), hint: t("rating.componentPenaltyHint"), value: -toNumeric(rating.PenaltyComponent) }, + { key: "manual", label: t("rating.componentManual"), hint: t("rating.componentManualHint"), value: toNumeric(rating.ManualComponent) } + ]; + const scale = Math.max(1, ...components.map((item) => Math.abs(item.value))); + // The score is clamped at zero, and a delayed increase sits in PendingStars + // instead of the score, so both cases are expected rather than drift. + const sum = Math.max(0, components.reduce((total, item) => total + item.value, 0)); + const total = toNumeric(rating.Stars); + const pending = toNumeric(rating.PendingStars); + + return ( + <> +
+ {components.map((item) => { + const percent = Math.min(100, (Math.abs(item.value) / scale) * 100); + const tone = item.value < 0 ? "danger" : item.value > 0 ? "good" : ""; + return ( +
+
+ {item.label} + {item.hint} +
+
+ +
+
{formatSigned(String(item.value))}
+
+ ); + })} +
+
{t("rating.componentTotal")}
+
{formatQuantity(rating.Stars)}
+
+
+ {pending === 0 && sum !== total && ( + {t("rating.breakdownMismatch", { sum: formatQuantity(String(sum)), total: formatQuantity(rating.Stars) })} + )} + {pending !== 0 &&

{t("rating.breakdownPending", { amount: formatSigned(rating.PendingStars) })}

} + + ); +} + +function EventKind({ kind }: { kind: AccountRatingEventKind }) { + const { t } = useI18n(); + const tone = kind === "moderation" ? "danger" : kind === "manual" ? "warn" : kind === "recompute" ? "neutral" : "good"; + return {t(`rating.kind.${kind}`)}; +} diff --git a/cmd/telesrv-admin/web/src/pages/AccountRatingsPage.tsx b/cmd/telesrv-admin/web/src/pages/AccountRatingsPage.tsx new file mode 100644 index 00000000..dc9cca74 --- /dev/null +++ b/cmd/telesrv-admin/web/src/pages/AccountRatingsPage.tsx @@ -0,0 +1,167 @@ +import { ChevronDown, ChevronRight, Loader2, RefreshCw, Search, Trophy } from "lucide-react"; +import { useEffect, useState } from "react"; +import { api, errorMessage } from "../api"; +import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui"; +import { useI18n } from "../i18n"; +import { displayUsername, formatDate, formatQuantity, toNumeric } from "../lib/format"; +import type { Navigate } from "../routing"; +import type { AccountRatingRow } from "../types"; + +export function AccountRatingsPage({ navigate }: { navigate: Navigate }) { + const { t } = useI18n(); + const [minLevel, setMinLevel] = useState(""); + const [search, setSearch] = useState(""); + const [limit, setLimit] = useState("50"); + const [rows, setRows] = useState([]); + const [hasMore, setHasMore] = useState(false); + const [cursor, setCursor] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + async function load(next = false) { + // One free-text field: the backend matches a username prefix (editable or + // collectible), a first/last name prefix, and a bare number as the user id. + const wanted = search.trim(); + setBusy(true); + setError(""); + const params = new URLSearchParams({ limit }); + if (minLevel.trim()) params.set("min_level", minLevel.trim()); + if (wanted) params.set("q", wanted); + if (next && cursor) params.set("before_id", cursor); + try { + const result = await api.accountRatings(params); + const page = result.rows ?? []; + setRows((current) => (next ? [...current, ...page] : page)); + setCursor(result.next_before_id ?? ""); + setHasMore(Boolean(result.has_more)); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + useEffect(() => { + void load(false); + }, []); + + const topLevel = rows.reduce((max, row) => Math.max(max, row.Level), 0); + const pendingCount = rows.filter((row) => toNumeric(row.PendingStars) !== 0).length; + const avgLevel = rows.length > 0 + ? (rows.reduce((sum, row) => sum + row.Level, 0) / rows.length).toFixed(1) + : "0"; + + return ( + load(false)} disabled={busy}> + {t("common.refresh")} + + } + > + {error && {error}} +
+ + + + +
+ + +
{ event.preventDefault(); void load(false); }}> + + + + +
+
+ +
+ + + + + + + + + + + + + + + {rows.map((row) => ( + + + + + + + + + + + ))} + {rows.length === 0 && } + +
{t("rating.userID")}{t("common.username")}{t("rating.level")}{t("rating.stars")}{t("rating.progress")}{t("rating.pending")}{t("rating.computedAt")}
{row.UserID}{displayUsername(row.Username) || row.FirstName || "-"}{formatQuantity(row.Stars)}{toNumeric(row.PendingStars) !== 0 ? formatQuantity(row.PendingStars) : "-"}{formatDate(row.ComputedAt) || "-"} + +
+
+ {hasMore && ( +
+ +
+ )} +
+ ); +} + +export function LevelBadge({ level }: { level: number }) { + const { t } = useI18n(); + const tone = level >= 10 ? "good" : level >= 5 ? "warn" : "neutral"; + return {t("rating.levelValue", { level })}; +} + +export function levelProgress(row: AccountRatingRow): { percent: number; remaining: number; target: number; stars: number } { + const stars = toNumeric(row.Stars); + const current = toNumeric(row.CurrentLevelStars); + const target = toNumeric(row.NextLevelStars); + const span = target - current; + const percent = span > 0 ? Math.min(100, Math.max(0, ((stars - current) / span) * 100)) : 0; + return { percent, remaining: Math.max(0, target - stars), target, stars }; +} + +export function RatingProgress({ row }: { row: AccountRatingRow }) { + const { t } = useI18n(); + if (!row.HasNextLevel) { + return {t("rating.maxLevel")}; + } + const { percent, remaining, target } = levelProgress(row); + return ( +
+
+ +
+ {t("rating.progressHint", { remaining: formatQuantity(String(remaining)), target: formatQuantity(String(target)) })} +
+ ); +} diff --git a/cmd/telesrv-admin/web/src/pages/AccountsPage.tsx b/cmd/telesrv-admin/web/src/pages/AccountsPage.tsx index 08d0d201..b71779c2 100644 --- a/cmd/telesrv-admin/web/src/pages/AccountsPage.tsx +++ b/cmd/telesrv-admin/web/src/pages/AccountsPage.tsx @@ -1,10 +1,10 @@ import { ChevronRight, Loader2, RefreshCw, Search } from "lucide-react"; import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; -import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui"; +import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel, UsernameCell } from "../components/ui"; import { ScamFakeBadges } from "../components/flags"; import { useI18n } from "../i18n"; -import { displayName, displayPhone, displayUsername, formatDate, formatUnix } from "../lib/format"; +import { displayName, displayPhone, formatDate, formatUnix } from "../lib/format"; import { accountMetrics } from "../lib/metrics"; import type { Navigate } from "../routing"; import type { AccountListResponse } from "../types"; @@ -107,7 +107,7 @@ export function AccountsPage({ navigate }: { navigate: Navigate }) { {row.ID} {displayPhone(row.Phone)} - {displayUsername(row.Username)} + {displayName(row)} {row.DeviceCount} {formatDate(row.LastActiveAt)} diff --git a/cmd/telesrv-admin/web/src/pages/BotVerificationPage.tsx b/cmd/telesrv-admin/web/src/pages/BotVerificationPage.tsx new file mode 100644 index 00000000..dc884125 --- /dev/null +++ b/cmd/telesrv-admin/web/src/pages/BotVerificationPage.tsx @@ -0,0 +1,965 @@ +import { + Ban, + BadgeCheck, + Building2, + ChevronDown, + ChevronRight, + ExternalLink, + Loader2, + Plus, + Power, + PowerOff, + RefreshCw, + Search, + Stamp, + Sticker, + Trash2 +} from "lucide-react"; +import { useEffect, useState, type ReactNode } from "react"; +import { api, APIError, errorMessage } from "../api"; +import { ActionButton } from "../components/ActionButton"; +import { BotPicker } from "../components/EntityPicker"; +import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel, SectionHead } from "../components/ui"; +import { useI18n } from "../i18n"; +import { displayUsername, formatDate } from "../lib/format"; +import { + permissionBotVerificationManage, + permissionVerificationReview, + usePermissions +} from "../permissions"; +import type { Navigate } from "../routing"; +import type { + BotRow, + BotVerificationPeerType, + BotVerifierRow, + CustomVerificationRequestRow, + CustomVerificationRequestStatus, + CustomVerificationRow, + VerificationIconRow +} from "../types"; + +type Tab = "requests" | "verifiers" | "icons" | "marks"; +type StatusFilter = "all" | CustomVerificationRequestStatus; +type PeerTypeFilter = "all" | BotVerificationPeerType; + +const statuses: CustomVerificationRequestStatus[] = ["pending", "approved", "rejected", "revoked"]; +const peerTypes: BotVerificationPeerType[] = ["user", "channel"]; + +// The section owns four different objects — applications, verifiers, the icon +// catalogue and the granted marks — and mixing them into one table would hide which +// row an action addresses. They are separate tabs over one shared verifier/icon +// load: the roster feeds three of the four filters, so it is fetched once here +// rather than per tab. +export function BotVerificationPage({ navigate }: { navigate: Navigate }) { + const { t } = useI18n(); + const { can } = usePermissions(); + const canManage = can(permissionBotVerificationManage); + const canSeeOfficial = can(permissionVerificationReview); + const [tab, setTab] = useState("requests"); + const [verifiers, setVerifiers] = useState([]); + const [icons, setIcons] = useState([]); + const [error, setError] = useState(""); + const [rosterDenied, setRosterDenied] = useState(false); + + async function loadRoster() { + setError(""); + setRosterDenied(false); + try { + const [verifierResult, iconResult] = await Promise.all([ + api.botVerifiers(new URLSearchParams({ limit: "200" })), + api.verificationIcons(new URLSearchParams({ limit: "200" })) + ]); + setVerifiers(verifierResult.rows ?? []); + setIcons(iconResult.rows ?? []); + } catch (err) { + // A 403 here is not a fault to alarm about: it means the session may review + // applications but not see the roster. Saying so beats an empty table that + // reads as "no verifiers configured". + if (err instanceof APIError && err.status === 403) { + setVerifiers([]); + setIcons([]); + setRosterDenied(true); + return; + } + setError(errorMessage(err)); + } + } + + useEffect(() => { + void loadRoster(); + }, []); + + const tabs: Array<{ key: Tab; label: string; icon: ReactNode }> = [ + { key: "requests", label: t("botverification.tabRequests"), icon: }, + { key: "verifiers", label: t("botverification.tabVerifiers"), icon: }, + { key: "icons", label: t("botverification.tabIcons"), icon: }, + { key: "marks", label: t("botverification.tabMarks"), icon: } + ]; + + return ( + navigate("/verification")}> + {t("botverification.openOfficial")} + + ) : undefined + } + > + {error && {error}} + {rosterDenied && {t("botverification.rosterDenied")}} + {/* The one thing an operator has to understand before touching anything here: + this is a verifier company's own icon, not the platform checkmark. */} +
+ +

{t("botverification.explainIcon")}

+

{t("botverification.explainOfficial")}

+ {!canManage &&

{t("botverification.manageMissing")}

} +
+ +
+ {tabs.map((item) => ( + + ))} +
+ + {tab === "requests" && } + {tab === "verifiers" && ( + + )} + {tab === "icons" && ( + + )} + {tab === "marks" && } +
+ ); +} + +// --------------------------------------------------------------------------- +// Applications +// --------------------------------------------------------------------------- + +function RequestsBlock({ navigate, verifiers }: { navigate: Navigate; verifiers: BotVerifierRow[] }) { + const { t } = useI18n(); + const [status, setStatus] = useState("pending"); + const [verifierBotID, setVerifierBotID] = useState(""); + const [peerType, setPeerType] = useState("all"); + const [q, setQ] = useState(""); + const [limit, setLimit] = useState("50"); + const [rows, setRows] = useState([]); + const [counts, setCounts] = useState>({}); + const [hasMore, setHasMore] = useState(false); + const [cursor, setCursor] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + // One free-text field: the backend matches the application id, the peer id and a + // username (applicant or peer), so "@durov", "42" and a peer id all work without a + // mode switch. + async function load(next = false) { + setBusy(true); + setError(""); + const params = new URLSearchParams({ limit }); + if (status !== "all") params.set("status", status); + if (verifierBotID) params.set("verifier_bot_id", verifierBotID); + if (peerType !== "all") params.set("peer_type", peerType); + if (q.trim()) params.set("q", q.trim().replace(/^@/, "")); + if (next && cursor) params.set("before_id", cursor); + try { + const result = await api.customVerificationRequests(params); + const page = result.rows ?? []; + setRows((current) => (next ? [...current, ...page] : page)); + setCursor(result.next_before_id ?? ""); + setHasMore(Boolean(result.has_more)); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + // The counts describe the whole queue, not the current page, so they are fetched + // separately from the keyset listing. + async function loadCounts() { + try { + const result = await api.botVerificationCounts(); + setCounts(result.counts ?? {}); + } catch (err) { + setError(errorMessage(err)); + } + } + + useEffect(() => { + void load(false); + void loadCounts(); + }, []); + + function refresh() { + void load(false); + void loadCounts(); + } + + return ( + <> +
+ + {t("common.refresh")} + + } + /> + {error && {error}} +
+ {statuses.map((item) => ( + + ))} +
+
+ + +
{ event.preventDefault(); void load(false); }}> + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + {rows.map((row) => ( + + + + + + + + + + + ))} + {rows.length === 0 && } + +
{t("common.id")}{t("botverification.verifier")}{t("botverification.target")}{t("botverification.applicant")}{t("botverification.reason")}{t("common.status")}{t("botverification.createdAt")}
+ + + {displayUsername(row.VerifierBotUsername) || row.VerifierBotID} +
{row.VerifierBotID}
+
+ {peerLabel(row)} +
+ {t(`botverification.peer.${row.PeerType}`)} · {row.PeerID} +
+
+ {displayUsername(row.ApplicantUsername) || "-"} +
{row.ApplicantUserID}
+
{row.Reason || "-"}{formatDate(row.CreatedAt) || "-"} + +
+
+ {hasMore && ( +
+ +
+ )} + + ); +} + +// --------------------------------------------------------------------------- +// Verifiers +// --------------------------------------------------------------------------- + +function VerifiersBlock({ + verifiers, + icons, + canManage, + onChanged, + navigate +}: { + verifiers: BotVerifierRow[]; + icons: VerificationIconRow[]; + canManage: boolean; + onChanged: () => void; + navigate: Navigate; +}) { + const { t } = useI18n(); + const [bot, setBot] = useState(null); + // editing carries the bot id of the row being updated: the grant endpoint is an + // upsert, and version is the optimistic lock of the row it overwrites. A fresh + // grant sends "0", which is what "there is no row yet" means. + const [editing, setEditing] = useState(null); + const [iconDocumentID, setIconDocumentID] = useState(""); + const [company, setCompany] = useState(""); + const [defaultDescription, setDefaultDescription] = useState(""); + const [canModify, setCanModify] = useState(false); + const activeIcons = icons.filter((icon) => icon.Active); + // A verifier can hold an icon the operator has since retired. Editing that row must + // not silently swap the icon just because the select has no matching option, so the + // current document is kept in the list and labelled instead. + const iconOptions: Array<{ value: string; label: string }> = activeIcons.map((icon) => ({ + value: icon.DocumentID, + label: `${icon.Name} · ${icon.DocumentID}` + })); + if (iconDocumentID && !iconOptions.some((option) => option.value === iconDocumentID)) { + const retired = icons.find((icon) => icon.DocumentID === iconDocumentID); + iconOptions.unshift({ + value: iconDocumentID, + label: `${retired?.Name ?? iconDocumentID} · ${iconDocumentID} (${t("botverification.iconInactive")})` + }); + } + + function startEdit(row: BotVerifierRow) { + setEditing(row); + setBot(null); + setIconDocumentID(row.IconDocumentID); + setCompany(row.CompanyName); + setDefaultDescription(row.DefaultDescription); + setCanModify(row.CanModifyCustomDescription); + } + + function resetForm() { + setEditing(null); + setBot(null); + setIconDocumentID(""); + setCompany(""); + setDefaultDescription(""); + setCanModify(false); + } + + // int64 fields go out as decimal strings (the backend tags them `,string`), which + // is also the shape they arrived in, so nothing is re-parsed on the way back. + function grantPayload(): Record { + const botID = editing ? editing.BotID : bot ? String(bot.ID) : "0"; + return { + bot_id: botID, + icon_document_id: iconDocumentID || "0", + company_name: company.trim(), + default_description: defaultDescription.trim(), + can_modify_custom_description: canModify, + version: editing ? editing.Version : "0" + }; + } + + return ( + <> + {canManage && ( +
+ + {t("botverification.cancelEdit")} + + ) : undefined + } + /> + {editing ? ( +

+ {t("botverification.editing", { + bot: displayUsername(editing.BotUsername) || editing.BotID, + version: editing.Version + })} +

+ ) : ( + + )} +
+ + + +
+ +

{t("botverification.canModifyHint")}

+ {activeIcons.length === 0 && {t("botverification.noActiveIcons")}} +
+ {t("botverification.grantNote")} + } + tone="neutral" + path="/api/actions/grant-bot-verifier" + payload={grantPayload} + onDone={() => { + resetForm(); + onChanged(); + }} + /> +
+
+ )} + +
+ + {t("common.refresh")} + + } + /> +
+ + + + + + + + + + + + {canManage && } + + + + {verifiers.map((row) => ( + + + + + + + + + + {canManage && ( + + )} + + ))} + {verifiers.length === 0 && } + +
{t("botverification.bot")}{t("botverification.company")}{t("botverification.icon")}{t("botverification.canModifyShort")}{t("common.status")}{t("botverification.markCount")}{t("botverification.grantedBy")}{t("common.updatedAt")}
+ +
{row.BotID}
+
+ {row.CompanyName || "-"} +
{row.DefaultDescription || t("botverification.notProvided")}
+
+ {row.IconName || "-"} +
{row.IconDocumentID}
+
{row.CanModifyCustomDescription ? t("common.yes") : t("common.no")} + {row.Enabled + ? {t("botverification.enabled")} + : {t("botverification.disabled")}} + {String(row.MarkCount ?? "0")} + {row.GrantedBy || "-"} +
{row.GrantReason || "-"}
+
{formatDate(row.UpdatedAt) || "-"} +
+ + : } + tone={row.Enabled ? "warn" : "neutral"} + compact + path="/api/actions/set-bot-verifier-enabled" + payload={() => ({ bot_id: row.BotID, enabled: !row.Enabled })} + onDone={onChanged} + /> + } + tone="danger" + compact + path="/api/actions/revoke-bot-verifier" + payload={() => ({ bot_id: row.BotID })} + onDone={onChanged} + /> +
+
+
+

{t("botverification.disableHint")}

+

{t("botverification.revokeVerifierHint")}

+
+ + ); +} + +// --------------------------------------------------------------------------- +// Icon catalogue +// --------------------------------------------------------------------------- + +function IconsBlock({ + icons, + verifiers, + canManage, + onChanged +}: { + icons: VerificationIconRow[]; + verifiers: BotVerifierRow[]; + canManage: boolean; + onChanged: () => void; +}) { + const { t } = useI18n(); + const [documentID, setDocumentID] = useState(""); + const [name, setName] = useState(""); + const [ownerBotID, setOwnerBotID] = useState(""); + + // owner_bot_id is omitted entirely for a shared entry rather than sent as "" — + // `,string,omitempty` cannot decode an empty string. + function iconPayload(): Record { + const payload: Record = { + document_id: documentID.trim() || "0", + name: name.trim() + }; + if (ownerBotID) payload.owner_bot_id = ownerBotID; + return payload; + } + + return ( + <> + {canManage && ( +
+ +
+ + + +
+

{t("botverification.iconDocumentHint")}

+

{t("botverification.iconOwnerHint")}

+
+ {t("botverification.addIconNote")} + } + tone="neutral" + path="/api/actions/upsert-verification-icon" + payload={iconPayload} + onDone={() => { + setDocumentID(""); + setName(""); + setOwnerBotID(""); + onChanged(); + }} + /> +
+
+ )} + +
+ + {t("common.refresh")} + + } + /> +
+ + + + + + + + + + {canManage && } + + + + {icons.map((row) => ( + + + + + + + + {canManage && ( + + )} + + ))} + {icons.length === 0 && } + +
{t("botverification.iconDocument")}{t("botverification.iconName")}{t("botverification.iconOwner")}{t("common.status")}{t("botverification.usedBy")}{t("botverification.createdAt")}
{row.DocumentID}{row.Name || "-"} + {row.OwnerBotID && row.OwnerBotID !== "0" + ? <> + {displayUsername(row.OwnerBotUsername) || row.OwnerBotID} +
{row.OwnerBotID}
+ + : {t("botverification.iconOwnerShared")}} +
+ {row.Active + ? {t("botverification.iconActive")} + : {t("botverification.iconInactive")}} + {String(row.UsedByVerifiers ?? "0")}{formatDate(row.CreatedAt) || "-"} +
+ : } + tone={row.Active ? "warn" : "neutral"} + compact + path="/api/actions/set-verification-icon-active" + payload={() => ({ icon_id: row.ID, active: !row.Active })} + onDone={onChanged} + /> +
+
+
+

{t("botverification.deactivateIconHint")}

+
+ + ); +} + +// --------------------------------------------------------------------------- +// Granted marks +// --------------------------------------------------------------------------- + +function MarksBlock({ + verifiers, + canManage, + navigate +}: { + verifiers: BotVerifierRow[]; + canManage: boolean; + navigate: Navigate; +}) { + const { t } = useI18n(); + const [verifierBotID, setVerifierBotID] = useState(""); + const [peerType, setPeerType] = useState("all"); + const [q, setQ] = useState(""); + const [limit, setLimit] = useState("50"); + const [rows, setRows] = useState([]); + const [hasMore, setHasMore] = useState(false); + const [cursor, setCursor] = useState(""); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + async function load(next = false) { + setBusy(true); + setError(""); + const params = new URLSearchParams({ limit }); + if (verifierBotID) params.set("verifier_bot_id", verifierBotID); + if (peerType !== "all") params.set("peer_type", peerType); + if (q.trim()) params.set("q", q.trim().replace(/^@/, "")); + if (next && cursor) params.set("before_id", cursor); + try { + const result = await api.customVerifications(params); + const page = result.rows ?? []; + setRows((current) => (next ? [...current, ...page] : page)); + setCursor(result.next_before_id ?? ""); + setHasMore(Boolean(result.has_more)); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + useEffect(() => { + void load(false); + }, []); + + return ( + <> +
+ load(false)} disabled={busy}> + {t("common.refresh")} + + } + /> + {error && {error}} +
+ + +
{ event.preventDefault(); void load(false); }}> + + + + + +
+
+ +
+ + + + + + + + + + {canManage && } + + + + {rows.map((row) => ( + + + + + + + + {canManage && ( + + )} + + ))} + {rows.length === 0 && } + +
{t("common.id")}{t("botverification.verifier")}{t("botverification.target")}{t("botverification.description")}{t("botverification.icon")}{t("botverification.createdAt")}
#{row.ID} + {row.CompanyName || displayUsername(row.VerifierBotUsername) || row.VerifierBotID} +
+ {displayUsername(row.VerifierBotUsername) || row.VerifierBotID} +
+
+ +
+ {t(`botverification.peer.${row.PeerType}`)} · {row.PeerID} +
+
{row.Description || t("botverification.notProvided")}{row.IconDocumentID}{formatDate(row.CreatedAt) || "-"} +
+ } + tone="danger" + compact + path="/api/actions/revoke-custom-verification" + payload={() => ({ + verifier_bot_id: row.VerifierBotID, + peer_type: row.PeerType, + peer_id: row.PeerID + })} + onDone={() => load(false)} + /> +
+
+
+

{t("botverification.revokeMarkHint")}

+ {hasMore && ( +
+ +
+ )} + + ); +} + +// --------------------------------------------------------------------------- +// Shared bits +// --------------------------------------------------------------------------- + +// The verifier filter lists the roster rather than asking for a bot id: a company +// name is what an operator reads in the queue, and a disabled verifier still owns +// rows worth filtering by, so it stays in the list and is labelled instead. +function VerifierOptions({ + value, + verifiers, + onChange +}: { + value: string; + verifiers: BotVerifierRow[]; + onChange: (value: string) => void; +}) { + const { t } = useI18n(); + return ( + + ); +} + +export function RequestStatusBadge({ status }: { status: CustomVerificationRequestStatus }) { + const { t } = useI18n(); + return {t(`botverification.status.${status}`)}; +} + +export function statusTone(status: CustomVerificationRequestStatus): "neutral" | "good" | "warn" | "danger" { + if (status === "approved") return "good"; + if (status === "pending") return "warn"; + if (status === "rejected") return "danger"; + return "neutral"; +} + +// pending is the only status that waits for somebody, so it is the only one +// highlighted — and only while something actually sits in it. +function countTone(status: CustomVerificationRequestStatus, count: string): "neutral" | "good" | "warn" { + if (status === "pending") return count !== "0" && count !== "" ? "warn" : "neutral"; + return status === "approved" ? "good" : "neutral"; +} + +export function peerLabel(row: { PeerUsername: string; PeerTitle: string; PeerID: string }): string { + return displayUsername(row.PeerUsername) || row.PeerTitle || `#${row.PeerID}`; +} + +// The panel page that owns the peer type. A third-party mark can sit on an ordinary +// account or on a bot — both are user rows, so both open the account page. +export function peerHref(peerType: BotVerificationPeerType, peerID: string): string { + return peerType === "channel" ? `/channels/${peerID}` : `/accounts/${peerID}`; +} diff --git a/cmd/telesrv-admin/web/src/pages/BotVerificationRequestPage.tsx b/cmd/telesrv-admin/web/src/pages/BotVerificationRequestPage.tsx new file mode 100644 index 00000000..9428a1e9 --- /dev/null +++ b/cmd/telesrv-admin/web/src/pages/BotVerificationRequestPage.tsx @@ -0,0 +1,360 @@ +import { + ArrowLeft, + BadgeCheck, + Ban, + Building2, + CheckCircle2, + ExternalLink, + RefreshCw, + ShieldOff, + Stamp, + User, + XCircle +} from "lucide-react"; +import { useEffect, useState, type ReactNode } from "react"; +import { api, APIError, errorMessage } from "../api"; +import { ActionButton } from "../components/ActionButton"; +import { Alert, Badge, LoadingSurface, PageFrame, SectionHead, SplitLayout, Summary } from "../components/ui"; +import { useI18n } from "../i18n"; +import { displayUsername, formatDate } from "../lib/format"; +import type { Navigate } from "../routing"; +import type { BotVerifierRow, CustomVerificationRequestDetail } from "../types"; +import { RequestStatusBadge, peerHref, peerLabel } from "./BotVerificationPage"; + +export function BotVerificationRequestPage({ id, navigate }: { id: string; navigate: Navigate }) { + const { t } = useI18n(); + const [detail, setDetail] = useState(null); + const [note, setNote] = useState(""); + const [conflict, setConflict] = useState(false); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + async function load() { + setBusy(true); + setError(""); + try { + setDetail(await api.customVerificationRequest(id)); + } catch (err) { + setError(errorMessage(err)); + } finally { + setBusy(false); + } + } + + function refresh() { + setConflict(false); + void load(); + } + + useEffect(() => { + void load(); + }, [id]); + + // 409 is the one failure the operator cannot fix by editing the form: another + // admin decided against the version this page read. The panel says so in plain + // words and reloads, so the next attempt carries the current version. + function handleActionError(err: unknown): string | undefined { + if (err instanceof APIError && err.status === 409) { + setConflict(true); + void load(); + return t("botverification.conflict"); + } + return undefined; + } + + if (error && !detail) { + return {error}; + } + if (!detail) { + return ; + } + + const request = detail.request; + const verifier = liveVerifier(detail.verifier); + const markActive = detail.mark_active; + const canDecide = request.Status === "pending"; + const canRevoke = request.Status === "approved"; + const trimmedNote = note.trim(); + // What the mark would actually say: the applicant's wording only when this + // verifier is allowed to override its own default, otherwise the default. Same + // rule the backend applies (BotVerifierSettings.DescriptionFor), shown here so a + // reviewer is not surprised by the text that ends up in the profile. + const requestedDescription = request.RequestedDescription.trim(); + const descriptionAllowed = Boolean(verifier?.CanModifyCustomDescription) && requestedDescription !== ""; + const effectiveDescription = descriptionAllowed + ? requestedDescription + : (verifier?.DefaultDescription ?? "").trim(); + + // version is the optimistic-locking token: it goes with every decision, as the + // decimal string it arrived as, so a stale page cannot overwrite a fresh one. + function decisionPayload(): Record { + const payload: Record = { version: request.Version }; + if (trimmedNote) payload.internal_note = trimmedNote; + return payload; + } + + function afterDecision() { + setNote(""); + setConflict(false); + void load(); + } + + return ( + + + + + } + > + {error && {error}} + {conflict && {t("botverification.conflict")}} + +
+
+
{peerLabel(request)}
+
+ #{request.ID} · {t(`botverification.peer.${request.PeerType}`)}:{request.PeerID} · v{request.Version} +
+
+
+ + {markActive + ? {t("botverification.markActive")} + : {t("botverification.markInactive")}} +
+
+ + {/* Repeated on the detail page on purpose: the decision an operator is + about to take grants a company's icon, not the platform badge. */} +
+ +

{t("botverification.explainIcon")}

+
+ +
+ navigate(`/bots/${request.VerifierBotID}`)}> + {t("botverification.openVerifier")} + + } + /> +
+ + + + + + +
+ + {verifier?.DefaultDescription + ?

{verifier.DefaultDescription}

+ :

{t("botverification.notProvided")}

} +
+ {!verifier && {t("botverification.verifierMissing")}} + {verifier && !verifier.Enabled && {t("botverification.verifierDisabledHint")}} +
+ +
+ navigate(peerHref(request.PeerType, request.PeerID))} + > + {t("botverification.openTarget")} + + } + /> +
+ + + + +
+
+ +
+ navigate(`/accounts/${request.ApplicantUserID}`)} + > + {t("botverification.openApplicant")} + + } + /> +
+ + + + +
+
+ +
+ +
+
+ + +
+ + {request.Reason + ?

{request.Reason}

+ :

{t("botverification.notProvided")}

} +
+ + {requestedDescription + ?

{requestedDescription}

+ :

{t("botverification.notProvided")}

} +
+ + {effectiveDescription + ?

{effectiveDescription}

+ :

{t("botverification.notProvided")}

} +
+

{t("botverification.markPreviewHint")}

+ {requestedDescription !== "" && !descriptionAllowed && ( +

{t("botverification.descriptionIgnoredHint")}

+ )} +
+
+ +
+ +
+
+ + + + +
+ + {request.DecisionReason + ?

{request.DecisionReason}

+ :

{t("botverification.noDecision")}

} +
+ {/* The internal note is the operator handover text and is labelled as + admin-only wherever it appears. */} + + {request.InternalNote + ?

{request.InternalNote}

+ :

{t("botverification.notProvided")}

} +
+
+
+ + } + side={ +
+
{t("botverification.actionDock")}
+ {!canDecide && !canRevoke &&

{t("botverification.noActions")}

} + {(canDecide || canRevoke) && ( + <> +