From 5ecf4e912d4a67c9c7ed44efa0060780972fab87 Mon Sep 17 00:00:00 2001 From: A Date: Thu, 16 Jul 2026 12:38:53 +0800 Subject: [PATCH] feat: sync collectible star gifts --- .env.example | 5 +- README.md | 2 +- README.zh-CN.md | 2 +- cmd/telesrv-admin/readstore.go | 56 ++ cmd/telesrv-admin/server.go | 383 +++++++++ .../web/dist/assets/index-CSxFgF7v.css | 1 + .../web/dist/assets/index-DRWO_DgE.js | 8 - .../web/dist/assets/index-Q8RNNOYL.js | 9 + .../web/dist/assets/index-W1-UPxwc.css | 1 - cmd/telesrv-admin/web/dist/index.html | 4 +- cmd/telesrv-admin/web/package-lock.json | 7 + cmd/telesrv-admin/web/package.json | 1 + cmd/telesrv-admin/web/src/api.ts | 16 +- .../web/src/components/Layout.tsx | 4 +- cmd/telesrv-admin/web/src/i18n.tsx | 156 ++++ cmd/telesrv-admin/web/src/lottie-web.d.ts | 4 + .../web/src/pages/GiftCollectiblesModal.tsx | 236 ++++++ cmd/telesrv-admin/web/src/pages/GiftsPage.tsx | 237 ++++++ cmd/telesrv-admin/web/src/pages/Routes.tsx | 4 + cmd/telesrv-admin/web/src/routing.ts | 2 + .../src/styles/03-entities-and-actions.css | 195 +++++ cmd/telesrv-admin/web/src/types.ts | 52 ++ cmd/telesrv/main.go | 6 +- .../0089_star_gift_catalog.down.sql | 17 + .../migrations/0089_star_gift_catalog.up.sql | 81 ++ .../0090_star_gift_collectibles.down.sql | 31 + .../0090_star_gift_collectibles.up.sql | 238 ++++++ .../0091_star_gift_prepaid_upgrade.down.sql | 3 + .../0091_star_gift_prepaid_upgrade.up.sql | 15 + .../0092_collectible_issuance_guard.down.sql | 21 + .../0092_collectible_issuance_guard.up.sql | 28 + docs/configuration.en.md | 2 +- docs/configuration.zh-CN.md | 2 +- internal/admin/service.go | 282 ++++++- internal/admin/service_test.go | 90 ++ internal/adminapi/server.go | 233 ++++++ internal/adminapi/server_test.go | 119 +++ internal/app/files/star_gifts_catalog.go | 97 --- internal/app/stargifts/animation.go | 190 +++++ internal/app/stargifts/animation_test.go | 93 +++ internal/app/stargifts/service.go | 377 ++++++++- internal/app/stargifts/service_test.go | 43 +- internal/domain/media.go | 48 +- internal/domain/message.go | 30 +- internal/domain/star_gift.go | 411 ++++++++- internal/domain/stars.go | 23 +- internal/rpc/convert_messages.go | 19 + internal/rpc/deps.go | 15 + internal/rpc/passive_compat_test.go | 4 +- internal/rpc/payments.go | 22 +- .../rpc/payments_star_gift_collections.go | 235 ++++++ internal/rpc/payments_star_gift_unique.go | 278 +++++++ internal/rpc/payments_star_gifts.go | 308 +++++-- internal/rpc/payments_star_gifts_rpc_test.go | 385 ++++++++- internal/store/memory/star_gift.go | 596 ++++++++++++- .../postgres/authkey_integration_test.go | 8 + internal/store/postgres/message_send.go | 66 +- .../store/postgres/read_model_listener.go | 17 +- internal/store/postgres/star_gift.go | 462 ++++++++++- .../store/postgres/star_gift_collectibles.go | 781 ++++++++++++++++++ ...star_gift_collectibles_integration_test.go | 429 ++++++++++ .../postgres/star_gift_integration_test.go | 65 +- internal/store/postgres/star_gift_upgrade.go | 365 ++++++++ internal/store/star_gift.go | 42 +- 64 files changed, 7559 insertions(+), 403 deletions(-) create mode 100644 cmd/telesrv-admin/web/dist/assets/index-CSxFgF7v.css delete mode 100644 cmd/telesrv-admin/web/dist/assets/index-DRWO_DgE.js create mode 100644 cmd/telesrv-admin/web/dist/assets/index-Q8RNNOYL.js delete mode 100644 cmd/telesrv-admin/web/dist/assets/index-W1-UPxwc.css create mode 100644 cmd/telesrv-admin/web/src/lottie-web.d.ts create mode 100644 cmd/telesrv-admin/web/src/pages/GiftCollectiblesModal.tsx create mode 100644 cmd/telesrv-admin/web/src/pages/GiftsPage.tsx create mode 100644 deploy/migrations/0089_star_gift_catalog.down.sql create mode 100644 deploy/migrations/0089_star_gift_catalog.up.sql create mode 100644 deploy/migrations/0090_star_gift_collectibles.down.sql create mode 100644 deploy/migrations/0090_star_gift_collectibles.up.sql create mode 100644 deploy/migrations/0091_star_gift_prepaid_upgrade.down.sql create mode 100644 deploy/migrations/0091_star_gift_prepaid_upgrade.up.sql create mode 100644 deploy/migrations/0092_collectible_issuance_guard.down.sql create mode 100644 deploy/migrations/0092_collectible_issuance_guard.up.sql delete mode 100644 internal/app/files/star_gifts_catalog.go create mode 100644 internal/app/stargifts/animation.go create mode 100644 internal/app/stargifts/animation_test.go create mode 100644 internal/rpc/payments_star_gift_collections.go create mode 100644 internal/rpc/payments_star_gift_unique.go create mode 100644 internal/store/postgres/star_gift_collectibles.go create mode 100644 internal/store/postgres/star_gift_collectibles_integration_test.go create mode 100644 internal/store/postgres/star_gift_upgrade.go diff --git a/.env.example b/.env.example index c8524daf..c88c2dc4 100644 --- a/.env.example +++ b/.env.example @@ -178,5 +178,8 @@ TELESRV_TRANSLATION_RATE_WINDOW=1m # TELESRV_AI_KIMI_TEMPERATURE=0.6 # Business automation reply provider: -# echo (default), template/quick_reply, or ai/compose_ai/kimi to reuse AI providers. +# echo (default/empty), template/quick_reply/quick-reply, or +# ai/compose_ai/ai_compose/aicompose/kimi to reuse TELESRV_AI_PROVIDERS. +# Custom provider names such as "ollama" are selected through TELESRV_AI_PROVIDERS; +# use TELESRV_BUSINESS_AI_PROVIDER=ai for those. TELESRV_BUSINESS_AI_PROVIDER=echo diff --git a/README.md b/README.md index 0885a2d2..3c6e74a5 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ codebase. | ✅ | Supergroups and channels | Create, join, leave, invite links, participants, admins, forum topics, linked discussion guests, history, send/edit/delete/read, reactions, public search, and previews. | | ✅ | Media and files | Upload, download, local blob storage, photos, documents, thumbnails, canonical GIFv conversion, external media fetch, web page previews, map tile cache hooks, profile/channel photos. | | ✅ | Stickers and reactions | Sticker/reaction catalog, seed support, saved GIFs, recent reactions, top reactions, default reactions, and moderation-oriented reaction paths. | -| ✅ | Gifts and stars | Star gifts and local stars ledger foundations for compatibility and future feature work. | +| ✅ | Gifts and stars | Dynamic star gift catalog, admin import tools, collectible/unique gift upgrade flows, prepaid upgrade tracking, and local stars ledger foundations. | | ✅ | Bots and mini apps | Bot service foundations, callbacks, inline helpers, webview/mini-app paths, a minimal Bot API gateway for libraries such as `python-telegram-bot`, persistent `getUpdates` delivery, and demo tools. | | ✅ | Calls and live streams | Private call signaling foundations, group call state, RTMP live streaming, scheduled video chats, channel `join_as`, SFU/TURN building blocks, liveness, and expiry workers. | | ✅ | Admin and operations | Admin API/UI backend, PostgreSQL migrations, Redis volatile state, retention workers, pprof/debug hooks, and load-test helpers. | diff --git a/README.zh-CN.md b/README.zh-CN.md index ef338132..1c226bdf 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -54,7 +54,7 @@ https://github.com/user-attachments/assets/25e651dc-a022-4d60-8b9b-ca3e8bfe216c | ✅ | 超级群与频道 | create、join、leave、邀请链接、成员、管理员、forum topics、关联讨论组 guest 访问、history、send/edit/delete/read、reactions、公开搜索和预览。 | | ✅ | 媒体与文件 | upload、download、本地 blob 存储、照片、文档、缩略图、规范 GIFv 转换、外链媒体抓取、网页预览、地图缩略图缓存、用户/频道头像。 | | ✅ | Stickers 与 Reactions | sticker/reaction catalog、seed 支持、saved GIFs、recent reactions、top reactions、default reactions、reaction moderation 相关路径。 | -| ✅ | Gifts 与 Stars | star gifts、本地 stars ledger 基础,用于兼容和后续功能扩展。 | +| ✅ | Gifts 与 Stars | 动态 star gift catalog、后台导入、收藏品/唯一礼物升级流程、预付升级跟踪,以及本地 stars ledger 基础。 | | ✅ | Bots 与 Mini Apps | bot 服务基础、callbacks、inline helpers、webview/mini-app 路径、适配 `python-telegram-bot` 等库的最小 Bot API gateway、持久化 `getUpdates` 投递队列和 demo 工具。 | | ✅ | 通话与直播 | 私聊通话信令基础、group call 状态、RTMP live stream、定时视频通话、频道 `join_as` 身份、SFU/TURN building blocks、liveness 与 expiry worker。 | | ✅ | 管理与运维 | Admin API/UI backend、PostgreSQL migrations、Redis 易失态、retention workers、pprof/debug hooks、load-test helpers。 | diff --git a/cmd/telesrv-admin/readstore.go b/cmd/telesrv-admin/readstore.go index e5b13ab4..dae46000 100644 --- a/cmd/telesrv-admin/readstore.go +++ b/cmd/telesrv-admin/readstore.go @@ -10,6 +10,8 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" + + "telesrv/internal/domain" ) const ( @@ -131,6 +133,60 @@ type ChannelDetail struct { AuditLogs []AuditLogRow } +type StarGiftRow struct { + GiftID int64 + RevisionID int64 + Revision int + Title string + Stars int64 + ConvertStars int64 + Enabled bool + SortOrder int + DocumentID int64 + SourceName string + SourceFormat string + AnimationSHA string + AnimationSize int64 + Width int + Height int + FrameRate float64 + ReceivedCount int64 + CreatedBy string + UpdatedAt time.Time +} + +func (s *readStore) ListStarGifts(ctx context.Context) ([]StarGiftRow, error) { + rows, err := s.pool.Query(ctx, ` +SELECT c.gift_id, r.id, r.revision, r.title, r.stars, r.convert_stars, + c.enabled, c.sort_order, r.document_id, r.source_name, r.source_format, + encode(r.animation_sha256, 'hex'), d.size, r.width, r.height, r.frame_rate, + (SELECT COUNT(*) FROM peer_star_gifts p WHERE p.gift_id = c.gift_id), + r.created_by, c.updated_at +FROM star_gift_catalog c +JOIN star_gift_catalog_revisions r ON r.id = c.active_revision_id +JOIN documents d ON d.id = r.document_id +ORDER BY c.sort_order, c.gift_id +LIMIT $1`, domain.MaxStarGiftCatalogSize) + if err != nil { + return nil, fmt.Errorf("list star gifts: %w", err) + } + defer rows.Close() + out := make([]StarGiftRow, 0) + for rows.Next() { + var row StarGiftRow + if err := rows.Scan( + &row.GiftID, &row.RevisionID, &row.Revision, &row.Title, &row.Stars, &row.ConvertStars, + &row.Enabled, &row.SortOrder, &row.DocumentID, &row.SourceName, &row.SourceFormat, + &row.AnimationSHA, &row.AnimationSize, &row.Width, &row.Height, &row.FrameRate, + &row.ReceivedCount, &row.CreatedBy, &row.UpdatedAt, + ); err != nil { + return nil, err + } + out = append(out, row) + } + return out, rows.Err() +} + func (s *readStore) SearchAccounts(ctx context.Context, q string) ([]AccountRow, error) { q = strings.TrimSpace(q) if q == "" { diff --git a/cmd/telesrv-admin/server.go b/cmd/telesrv-admin/server.go index f61e3a6f..c72ab72b 100644 --- a/cmd/telesrv-admin/server.go +++ b/cmd/telesrv-admin/server.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "io/fs" + "mime/multipart" "net/http" "path" "strconv" @@ -56,6 +57,10 @@ func (s *server) routes() http.Handler { mux.Handle("GET /api/messages/detail", s.requireAuthAPI(http.HandlerFunc(s.handleMessageDetailAPI))) mux.Handle("GET /api/messages/groups", s.requireAuthAPI(http.HandlerFunc(s.handleGroupMessagesAPI))) mux.Handle("GET /api/messages/groups/detail", s.requireAuthAPI(http.HandlerFunc(s.handleGroupMessageDetailAPI))) + mux.Handle("GET /api/gifts", s.requireAuthAPI(http.HandlerFunc(s.handleStarGiftsAPI))) + 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("POST /api/actions/set-frozen", s.requireAuthAPI(http.HandlerFunc(s.handleSetAccountFrozenAPI))) mux.Handle("POST /api/actions/grant-premium", s.requireAuthAPI(http.HandlerFunc(s.handleGrantPremiumAPI))) mux.Handle("POST /api/actions/grant-stars", s.requireAuthAPI(http.HandlerFunc(s.handleGrantStarsAPI))) @@ -64,6 +69,10 @@ func (s *server) routes() http.Handler { mux.Handle("POST /api/actions/revoke-sessions", s.requireAuthAPI(http.HandlerFunc(s.handleRevokeSessionsAPI))) mux.Handle("POST /api/actions/delete-messages", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteMessagesAPI))) mux.Handle("POST /api/actions/delete-history", s.requireAuthAPI(http.HandlerFunc(s.handleDeleteHistoryAPI))) + mux.Handle("POST /api/actions/import-gift", s.requireAuthAPI(http.HandlerFunc(s.handleImportStarGiftAPI))) + mux.Handle("POST /api/actions/publish-gift-collectibles", s.requireAuthAPI(http.HandlerFunc(s.handlePublishStarGiftCollectiblesAPI))) + 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.HandleFunc("/api/", func(w http.ResponseWriter, _ *http.Request) { writeAPIError(w, http.StatusNotFound, "api route not found") }) @@ -163,6 +172,101 @@ func (s *server) handleSession(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]any{"actor": actorFromContext(r.Context())}) } +func (s *server) handleStarGiftsAPI(w http.ResponseWriter, r *http.Request) { + if s.read == nil { + writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") + return + } + rows, err := s.read.ListStarGifts(r.Context()) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{"Gifts": rows}) +} + +func (s *server) handleStarGiftAnimationAPI(w http.ResponseWriter, r *http.Request) { + giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil || giftID <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid gift id") + return + } + req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, + fmt.Sprintf("%s/v1/gifts/%d/animation", s.cfg.AdminAPIURL, giftID), nil) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + req.Header.Set("Authorization", "Bearer "+s.cfg.AdminAPIToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + writeAPIError(w, http.StatusBadGateway, err.Error()) + return + } + defer resp.Body.Close() + raw, err := io.ReadAll(io.LimitReader(resp.Body, (4<<20)+1)) + if err != nil || len(raw) > 4<<20 { + writeAPIError(w, http.StatusBadGateway, "invalid animation response") + return + } + if resp.StatusCode != http.StatusOK { + writeAPIError(w, resp.StatusCode, string(raw)) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Cache-Control", "private, max-age=60") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(raw) +} + +func (s *server) handleStarGiftCollectiblesAPI(w http.ResponseWriter, r *http.Request) { + giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil || giftID <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid gift id") + return + } + s.proxyAdminJSON(w, r, fmt.Sprintf("/v1/gifts/%d/collectibles", giftID), 4<<20) +} + +func (s *server) handleStarGiftCollectibleAnimationAPI(w http.ResponseWriter, r *http.Request) { + giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + attributeID, attrErr := strconv.ParseInt(r.PathValue("attribute_id"), 10, 64) + kind := r.PathValue("kind") + if err != nil || giftID <= 0 || attrErr != nil || attributeID <= 0 || (kind != "model" && kind != "pattern") { + writeAPIError(w, http.StatusBadRequest, "invalid collectible animation") + return + } + s.proxyAdminJSON(w, r, fmt.Sprintf("/v1/gifts/%d/collectibles/%s/%d/animation", giftID, kind, attributeID), 4<<20) +} + +func (s *server) proxyAdminJSON(w http.ResponseWriter, r *http.Request, apiPath string, maxBytes int64) { + req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, s.cfg.AdminAPIURL+apiPath, nil) + if err != nil { + writeAPIError(w, http.StatusInternalServerError, err.Error()) + return + } + req.Header.Set("Authorization", "Bearer "+s.cfg.AdminAPIToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + writeAPIError(w, http.StatusBadGateway, err.Error()) + return + } + defer resp.Body.Close() + raw, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1)) + if err != nil || int64(len(raw)) > maxBytes { + writeAPIError(w, http.StatusBadGateway, "invalid admin api response") + return + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + writeAPIError(w, resp.StatusCode, string(raw)) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Cache-Control", "private, max-age=30") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(raw) +} + func (s *server) handleAccountsAPI(w http.ResponseWriter, r *http.Request) { if s.read == nil { writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured") @@ -588,6 +692,184 @@ func (s *server) handleDeleteHistoryAPI(w http.ResponseWriter, r *http.Request) writeCommandResultAPI(w, result, err) } +type importStarGiftAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + GiftID int64 `json:"gift_id"` + Title string `json:"title"` + Stars int64 `json:"stars"` + ConvertStars int64 `json:"convert_stars"` + Enabled bool `json:"enabled"` + SortOrder int `json:"sort_order"` +} + +func (s *server) handleImportStarGiftAPI(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + r.Body = http.MaxBytesReader(w, r.Body, 5<<20) + if err := r.ParseMultipartForm(1 << 20); err != nil { + writeAPIError(w, http.StatusBadRequest, "invalid multipart form: "+err.Error()) + return + } + if r.MultipartForm != nil { + defer r.MultipartForm.RemoveAll() + } + var body importStarGiftAPIRequest + dec := json.NewDecoder(strings.NewReader(r.FormValue("metadata"))) + dec.DisallowUnknownFields() + if err := dec.Decode(&body); err != nil { + writeAPIError(w, http.StatusBadRequest, "invalid metadata: "+err.Error()) + return + } + file, header, err := r.FormFile("file") + if err != nil { + writeAPIError(w, http.StatusBadRequest, "animation file is required") + return + } + defer file.Close() + data, err := io.ReadAll(io.LimitReader(file, (4<<20)+1)) + if err != nil || len(data) == 0 || len(data) > 4<<20 { + writeAPIError(w, http.StatusBadRequest, "animation file is empty or too large") + return + } + req := admin.ImportStarGiftRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "import-gift"), + GiftID: body.GiftID, + Title: body.Title, + Stars: body.Stars, + ConvertStars: body.ConvertStars, + Enabled: body.Enabled, + SortOrder: body.SortOrder, + FileName: header.Filename, + } + result, err := s.callAdminMultipart(r.Context(), "/v1/gifts/import", req, header.Filename, data) + writeCommandResultAPI(w, result, err) +} + +type publishStarGiftCollectiblesAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + UpgradeStars int64 `json:"upgrade_stars"` + SupplyTotal int `json:"supply_total"` + SlugPrefix string `json:"slug_prefix"` + Models []admin.StarGiftCollectibleAnimationUpload `json:"models"` + Patterns []admin.StarGiftCollectibleAnimationUpload `json:"patterns"` + Backdrops []admin.StarGiftCollectibleBackdropInput `json:"backdrops"` +} + +func (s *server) handlePublishStarGiftCollectiblesAPI(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + giftID, err := strconv.ParseInt(r.URL.Query().Get("gift_id"), 10, 64) + if err != nil || giftID <= 0 { + writeAPIError(w, http.StatusBadRequest, "invalid gift id") + return + } + r.Body = http.MaxBytesReader(w, r.Body, 64<<20) + if err := r.ParseMultipartForm(8 << 20); err != nil { + writeAPIError(w, http.StatusBadRequest, "invalid collectible multipart form: "+err.Error()) + return + } + if r.MultipartForm != nil { + defer r.MultipartForm.RemoveAll() + } + var body publishStarGiftCollectiblesAPIRequest + dec := json.NewDecoder(strings.NewReader(r.FormValue("metadata"))) + dec.DisallowUnknownFields() + if err := dec.Decode(&body); err != nil { + writeAPIError(w, http.StatusBadRequest, "invalid metadata: "+err.Error()) + return + } + if len(body.Models)+len(body.Patterns) > 128 { + writeAPIError(w, http.StatusBadRequest, "too many collectible animation files") + return + } + seen := make(map[string]struct{}, len(body.Models)+len(body.Patterns)) + load := func(upload *admin.StarGiftCollectibleAnimationUpload) error { + upload.FileKey = strings.TrimSpace(upload.FileKey) + if upload.FileKey == "" { + return errors.New("animation file key is required") + } + if _, ok := seen[upload.FileKey]; ok { + return fmt.Errorf("duplicate animation file key %q", upload.FileKey) + } + seen[upload.FileKey] = struct{}{} + file, header, err := r.FormFile(upload.FileKey) + if err != nil { + return fmt.Errorf("animation file %q is required", upload.FileKey) + } + defer file.Close() + data, err := io.ReadAll(io.LimitReader(file, (4<<20)+1)) + if err != nil || len(data) == 0 || len(data) > 4<<20 { + return fmt.Errorf("animation file %q is empty or too large", upload.FileKey) + } + upload.FileName = header.Filename + upload.Data = data + return nil + } + for i := range body.Models { + if err := load(&body.Models[i]); err != nil { + writeAPIError(w, http.StatusBadRequest, err.Error()) + return + } + } + for i := range body.Patterns { + if err := load(&body.Patterns[i]); err != nil { + writeAPIError(w, http.StatusBadRequest, err.Error()) + return + } + } + req := admin.PublishStarGiftCollectiblesRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "publish-gift-collectibles"), + GiftID: giftID, UpgradeStars: body.UpgradeStars, SupplyTotal: body.SupplyTotal, + SlugPrefix: body.SlugPrefix, Models: body.Models, Patterns: body.Patterns, Backdrops: body.Backdrops, + } + result, err := s.callAdminCollectibleMultipart(r.Context(), fmt.Sprintf("/v1/gifts/%d/collectibles/publish", giftID), req) + writeCommandResultAPI(w, result, err) +} + +type setStarGiftEnabledAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + GiftID int64 `json:"gift_id"` + Enabled bool `json:"enabled"` +} + +func (s *server) handleSetStarGiftEnabledAPI(w http.ResponseWriter, r *http.Request) { + var body setStarGiftEnabledAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetStarGiftEnabledRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-gift-enabled"), + GiftID: body.GiftID, Enabled: body.Enabled, + } + result, err := s.callAdminAPI(r.Context(), "/v1/gifts/set-enabled", req) + writeCommandResultAPI(w, result, err) +} + +type setStarGiftSortOrderAPIRequest struct { + CommandID string `json:"command_id"` + Reason string `json:"reason"` + Confirm bool `json:"confirm"` + GiftID int64 `json:"gift_id"` + SortOrder int `json:"sort_order"` +} + +func (s *server) handleSetStarGiftSortOrderAPI(w http.ResponseWriter, r *http.Request) { + var body setStarGiftSortOrderAPIRequest + if !decodeAction(w, r, &body) { + return + } + req := admin.SetStarGiftSortOrderRequest{ + CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "set-gift-sort-order"), + GiftID: body.GiftID, SortOrder: body.SortOrder, + } + result, err := s.callAdminAPI(r.Context(), "/v1/gifts/set-sort-order", req) + writeCommandResultAPI(w, result, err) +} + 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-") { @@ -639,6 +921,107 @@ func (s *server) callAdminAPI(ctx context.Context, apiPath string, payload any) return result, 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) + meta, err := json.Marshal(metadata) + if err != nil { + return admin.CommandResult{}, err + } + if err := writer.WriteField("metadata", string(meta)); err != nil { + return admin.CommandResult{}, err + } + part, err := writer.CreateFormFile("file", fileName) + if err != nil { + return admin.CommandResult{}, err + } + if _, err := part.Write(data); err != nil { + return admin.CommandResult{}, err + } + if err := writer.Close(); err != nil { + return admin.CommandResult{}, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.cfg.AdminAPIURL+apiPath, &body) + if err != nil { + return admin.CommandResult{}, err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Authorization", "Bearer "+s.cfg.AdminAPIToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return admin.CommandResult{}, 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, 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, errors.New(result.Error) + } + return result, nil +} + +func (s *server) callAdminCollectibleMultipart(ctx context.Context, apiPath string, payload admin.PublishStarGiftCollectiblesRequest) (admin.CommandResult, error) { + var body bytes.Buffer + writer := multipart.NewWriter(&body) + meta, err := json.Marshal(payload) + if err != nil { + return admin.CommandResult{}, err + } + if err := writer.WriteField("metadata", string(meta)); err != nil { + return admin.CommandResult{}, err + } + writeUploads := func(uploads []admin.StarGiftCollectibleAnimationUpload) error { + for _, upload := range uploads { + part, err := writer.CreateFormFile(upload.FileKey, upload.FileName) + if err != nil { + return err + } + if _, err := part.Write(upload.Data); err != nil { + return err + } + } + return nil + } + if err := writeUploads(payload.Models); err != nil { + return admin.CommandResult{}, err + } + if err := writeUploads(payload.Patterns); err != nil { + return admin.CommandResult{}, err + } + if err := writer.Close(); err != nil { + return admin.CommandResult{}, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.cfg.AdminAPIURL+apiPath, &body) + if err != nil { + return admin.CommandResult{}, err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Authorization", "Bearer "+s.cfg.AdminAPIToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return admin.CommandResult{}, 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, 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, errors.New(result.Error) + } + return result, nil +} + func decodeAction(w http.ResponseWriter, r *http.Request, dst any) bool { if err := decodeJSON(r, dst); err != nil { writeAPIError(w, http.StatusBadRequest, err.Error()) diff --git a/cmd/telesrv-admin/web/dist/assets/index-CSxFgF7v.css b/cmd/telesrv-admin/web/dist/assets/index-CSxFgF7v.css new file mode 100644 index 00000000..e211c553 --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-CSxFgF7v.css @@ -0,0 +1 @@ +:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#f3f5f7;--panel:#fff;--panel-subtle:#f8fafb;--panel-strong:#eef2f5;--line:#d9e1e8;--line-strong:#c2ccd6;--text:#101828;--muted:#667085;--muted-2:#98a2b3;--brand:#176d61;--brand-2:#245b9d;--good:#167447;--warn:#a15c07;--danger:#b42318;--sidebar:#11161d;--sidebar-soft:#1b222b;--sidebar-line:#2c3541;--focus:#176d6129;--shadow:0 18px 52px #10182824}*{box-sizing:border-box}html,body,#root{min-height:100%}body{color:var(--text);background:var(--bg);margin:0;font:13px/1.45 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}button,input,textarea{font:inherit}a{color:inherit;text-decoration:none}.shell{grid-template-columns:232px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{color:#eef2f6;background:var(--sidebar);border-right:1px solid var(--sidebar-line);flex-direction:column;gap:16px;height:100vh;padding:18px 12px;display:flex;position:sticky;top:0}.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:0 8px 24px #176d6142}.brand-mark{color:#fff;background:var(--brand);border:1px solid #fff3;border-radius:8px;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:#aeb8c4;margin-top:3px;font-size:11px;display:block}.sidebar-label{color:#8492a6;text-transform:uppercase;padding:0 8px;font-size:11px;font-weight:700}.nav-list,.nav-section{gap:4px;display:grid}.nav-section-toggle{color:#8fa0b4;cursor:pointer;text-align:left;background:0 0;border:1px solid #0000;border-radius:7px;grid-template-columns:18px minmax(0,1fr) 16px;align-items:center;gap:9px;width:100%;min-height:38px;padding:0 10px;font-size:12px;font-weight:800;display:grid}.nav-section-toggle:hover,.nav-section.active .nav-section-toggle{color:#fff;background:var(--sidebar-soft);border-color:#34404d}.nav-section-chevron{color:#8fa0b4;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{color:#c6d0dc;border:1px solid #0000;border-radius:7px;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:9px;min-height:38px;padding:0 10px;display:grid}.nav-dot{background:#687789;border-radius:999px;justify-self:center;width:6px;height:6px}.nav-item:hover,.nav-item.active{color:#fff;background:var(--sidebar-soft);border-color:#34404d}.nav-item.active .nav-dot{background:var(--brand)}.sidebar-status{gap:7px;margin-top:auto;display:grid}.runtime-row{color:#cbd5df;background:#171d25;border:1px solid #27313c;border-radius:7px;grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;min-height:32px;padding:0 8px;display:grid}.runtime-row strong{color:#fff;font-size:11px}.workspace{min-width:0}.topbar{z-index:20;border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);background:#fffffff0;justify-content:space-between;align-items:center;gap:18px;min-height:66px;padding:12px 24px;display:flex;position:sticky;top:0}.topbar h1{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}.language-switch button.active{color:#fff;background:var(--brand)}.language-switch button:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.actor-pill{color:#344054;background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;min-height:30px;padding:0 10px;display:inline-flex}.content{gap:16px;padding:18px 24px 30px;display:grid}.eyebrow{color:var(--muted);text-transform:uppercase;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:8px;min-width:0}.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{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:7px;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:#afd8bf}.status-item.warn,.metric.warn{border-color:#e7c77e}.metric.danger{border-color:#efb4ad}.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:8px;grid-template-columns:38px minmax(0,1fr) 18px;align-items:center;gap:12px;min-height:94px;padding:14px;display:grid}.launcher:hover{border-color:var(--brand)}.launcher-icon{width:38px;height:38px;color:var(--brand);background:#edf7f4;border:1px solid #c9e2dc;border-radius:8px;place-items:center;display:grid}.launcher-copy{gap:4px;display:grid}.launcher-copy strong{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{color:#344054;background:var(--panel);border:1px solid var(--line);border-radius:8px;align-items:center;gap:8px;min-height:38px;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:8px;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{border:1px solid var(--line);background:#fff;border-radius:8px;gap:8px;min-width:0;padding:10px;display:grid}.picker-head{color:#344054;justify-content:space-between;align-items:center;gap:8px;min-height:24px;font-weight:800;display:flex}.selected-entity{color:#0f3f38;background:#eef8f5;border:1px solid #b9dcd3;border-radius:7px;grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:8px;min-height:40px;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:#52606d;font-size:11px}.picker-search{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:7px;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:7px;max-height:236px;display:grid;overflow:auto}.picker-row{min-height:36px;color:var(--text);border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;background:#fff;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:#f3f8f6}.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:#fff2f0;border:1px solid #efb4ad;border-radius:7px}input,textarea{color:var(--text);border:1px solid var(--line-strong);background:#fff;border-radius:7px;outline:none}input{width:190px;height:34px;padding:0 10px}textarea{resize:vertical;width:100%;padding:9px 10px}input: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{border:1px solid var(--line-strong);background:#fff;border-radius:7px;align-items:center;gap:8px;width:min(380px,100%);height:34px;padding:0 10px;display:inline-flex}.searchbox input{width:100%;height:30px;box-shadow:none;border:0;padding:0}.btn{color:#1d2939;border:1px solid var(--line-strong);cursor:pointer;white-space:nowrap;background:#fff;border-radius:7px;justify-content:center;align-items:center;gap:6px;min-height:34px;padding:0 12px;display:inline-flex}.btn:hover:not(:disabled){background:#f7f9fb}.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:#12594f}.btn.ghost{background:var(--panel-subtle)}.btn.danger{color:var(--danger);background:#fff7f5;border-color:#efb4ad}.btn.danger:hover:not(:disabled){background:#ffeceb}.btn.warn{color:var(--warn);background:#fff8ec;border-color:#e7c77e}.btn.warn:hover:not(:disabled){background:#fff1d6}.btn:disabled,.btn.primary:disabled,.btn.warn:disabled,.btn.danger:disabled{color:var(--muted-2);border-color:var(--line);cursor:not-allowed;background:#f3f5f7}.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{border:1px solid var(--line);border-radius:8px;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:#475467;background:var(--panel-strong);font-weight:800;position:sticky;top:0}.data-table tbody tr:hover{background:#fbfcfd}.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{color:#4f5b68;white-space:nowrap;background:#f3f6f8;border:1px solid #d7e0e8;border-radius:999px;align-items:center;min-height:22px;padding:1px 8px;display:inline-flex}.badge.good{color:var(--good);background:#eef8f2;border-color:#b9dcc7}.badge.danger{color:var(--danger);background:#fff2f0;border-color:#efb4ad}.badge.warn{color:var(--warn);background:#fff8e7;border-color:#e7c77e}.empty-cell{color:var(--muted);text-align:center}.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:8px;justify-content:space-between;align-items:flex-start;gap:14px;padding:14px;display:flex}.entity-title{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:#344054;border:1px solid var(--line);background:#fbfcfd;border-radius:8px;margin:0;padding:10px}.section-block,.action-dock,.surface{background:var(--panel);border:1px solid var(--line);border-radius:8px;min-width:0;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:#344054;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:8px;flex-wrap:wrap;align-items:center;gap:8px;padding:10px;display:flex}.operation-title{align-items:center;gap:6px;width:100%;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:#8a251d;background:#fff2f0;border:1px solid #efb4ad;border-radius:8px;align-items:flex-start;gap:8px;padding:9px 10px;display:flex}.json-block{color:#d8e6f0;background:#141a22;border:1px solid #2a3542;border-radius:8px;max-height:520px;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:8px;place-items:center;display:grid}.gift-metrics .metric{background:linear-gradient(145deg,#fff,#f6f9f9);min-height:68px;padding:12px}.gift-metrics .metric strong{font-size:17px}.gift-file-icon{color:var(--brand);background:#eaf6f3;border:1px solid #c7e3dc;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:#33645d;letter-spacing:.02em;background:#eef8f5;border:1px solid #cfe5df;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-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);cursor:pointer;background:#fff;border:1px dashed #b7ccc8;border-radius:10px;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{border-color:var(--brand);background:#f8fcfb;box-shadow:0 0 0 2px #176d610d}.gift-file-picker input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.gift-file-icon{border-radius:9px;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{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:#f0f8f6;border:1px solid #c7e3dc;border-radius:7px;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{min-width:0;height:38px;color:var(--text);border:1px solid var(--line);background:#fff;border-radius:7px;padding:0 10px}.gift-fields-grid input:focus,.gift-reason-field input:focus{border-color:#77b6aa;outline:none;box-shadow:0 0 0 3px #176d6114}.gift-switch{color:#344054;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:#c8d0d5;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-offset:2px;outline:3px solid #176d6129}.gift-validation{color:#d5fff5;background:#173631;border:1px solid #24564e;border-radius:9px;overflow:hidden}.gift-validation-head{color:#e3fff9;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:#99cfc4;font-size:10px}.gift-validation pre{color:#d5fff5;max-height:180px;margin:0;padding:11px 12px;font-size:11px;overflow:auto}.gift-animation-shell{background:radial-gradient(circle,#f9f3ff,#eef8f5);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);border:1px solid var(--line);background:#ffffffe6;border-radius:50%;place-items:center;display:grid;position:absolute;bottom:8px;right:8px}.gift-table-wrap{background:#fff}.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:9px;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:#755b00}.gift-table-actions{align-items:center;gap:6px;display:flex}.collectible-button{color:#6548a8;background:#f7f3ff;border-color:#ddd2f5}.collectible-button:hover{background:#efe8ff;border-color:#cbbaf0}.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:#f5f7fa;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:#66568c;background:linear-gradient(135deg,#fbf9ff,#f2f7ff);border:1px dashed #cfc3e9;border-radius:12px;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:#fff;border:1px solid #ddd6ee;border-radius:12px;overflow:hidden;box-shadow:0 5px 16px #422e6e0d}.collectible-active-head{background:linear-gradient(100deg,#fbf9ff,#f4f9ff);border-bottom:1px solid #e9e4f3;justify-content:space-between;align-items:center;gap:12px;padding:12px 14px;display:flex}.collectible-active-head>div{color:#60458f;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:#fff;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{border:1px solid var(--line);background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 8px 24px #1018280a}.collectible-definition-head{border-bottom:1px solid var(--line);background:linear-gradient(110deg,#f8fbfa,#fbf9ff);justify-content:space-between;align-items:center;gap:12px;padding:14px 16px;display:flex}.collectible-main-fields{border-bottom:1px solid var(--line);background:#fbfcfd;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:#fafbfc;border:1px solid #e1e6eb;border-radius:9px;align-items:end;gap:7px;padding:9px 9px 9px 36px;display:grid;position:relative}.collectible-row:hover{background:#fff;border-color:#cbd7dd;box-shadow:0 3px 10px #10182809}.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{color:#71668c;background:#f0edf7;border-right:1px solid #e0d9ed;border-radius:8px 0 0 8px;place-items:center;width:27px;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);font:inherit;background:#fff;border:1px solid #d5dde3;border-radius:7px;padding:0 8px;font-size:11px}.collectible-row input:focus{border-color:#8d7aba;outline:none;box-shadow:0 0 0 3px #6f5bae14}.collectible-file input{opacity:0;pointer-events:none;width:1px;height:1px;position:absolute}.collectible-file em{color:#625080;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;background:#f7f4fd;border:1px dashed #cfc4e1;border-radius:7px;align-items:center;gap:5px;min-width:0;height:32px;padding:0 8px;font-size:10px;font-style:normal;font-weight:700;display:flex;overflow:hidden}.collectible-inline-preview{color:#8c7cae;background:radial-gradient(circle,#fff,#eee8f8);border:1px solid #ded5ed;border-radius:8px;place-items:center;width:42px;height:42px;display:grid;overflow:hidden}.collectible-animation{width:100%;height:100%;overflow:hidden}.collectible-animation.compact{background:radial-gradient(circle,#fff,#f0ebfa);border:1px solid #e0d9ec;border-radius:8px;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:#b42318;background:#fff4f2}.collectible-animation.loading{color:#807397}.collectible-file-error{color:#b42318;grid-column:1/-1;font-size:10px}.collectible-color input{cursor:pointer;height:32px!important;padding:3px!important}.collectible-backdrop-preview{border:1px solid #2a1f472e;border-radius:8px;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,.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}.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}}.modal-backdrop{z-index:10000;background:#11182785;place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{border:1px solid var(--line);width:min(760px,100%);max-height:min(820px,100vh - 48px);box-shadow:var(--shadow);background:#fff;border-radius:8px;padding:0;overflow:hidden}.command-modal{flex-direction:column;display:flex}.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{background:var(--panel-subtle);border:1px solid var(--line);cursor:pointer;border-radius:7px;place-items:center;width:30px;height:30px;display:grid}.command-steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.command-body{gap:12px;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:8px;align-items:center;gap:8px;padding:0 10px;display:flex}.command-step span{border:1px solid var(--line);background:#fff;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:#a9d8ce}.command-step.done{color:var(--good);border-color:#b9dcc7}.form-field{gap:6px;display:grid}.form-field span,.form-stack span{color:#4b5563;font-weight:800}.command-preview{gap:8px;display:grid}.command-preview .json-block{max-height:150px}.preview-head,.result-title{color:#344054;align-items:center;gap:7px;font-weight:800;display:flex}.result-box{border:1px solid var(--line);background:#fbfcfd;border-radius:8px;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:#344054}.modal-actions{border-top:1px solid var(--line);background:#fff;justify-content:flex-end;padding:12px 18px}.login-page{background:var(--bg);place-items:center;min-height:100vh;padding:24px;display:grid}.login-panel{border:1px solid var(--line);width:min(420px,100%);box-shadow:var(--shadow);background:#fff;border-radius:8px;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:#edf7f4;border:1px solid #c9e2dc;border-radius:999px;align-items:center;padding:0 8px;font-size:12px;display:inline-flex}.login-copy h1{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{align-content:center;place-items:center;gap:18px;min-height:100vh;display:grid}.loader-bar{background:#d7dde4;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-DRWO_DgE.js b/cmd/telesrv-admin/web/dist/assets/index-DRWO_DgE.js deleted file mode 100644 index 6b1905fd..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-DRWO_DgE.js +++ /dev/null @@ -1,8 +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,ee=Object.prototype.hasOwnProperty,S={current:null},te={key:!0,ref:!0,__self:!0,__source:!0};function C(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)ee.call(n,i)&&!te.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,se(x);else{var t=n(l);t!==null&&ce(b,t.startTime-e)}}function x(t,i){m=!1,h&&(h=!1,_(te),te=-1),p=!0;var a=f;try{for(y(i),d=n(c);d!==null&&(!(d.expirationTime>i)||t&&!ne());){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&&ce(b,g.startTime-i),u=!1}return u}finally{d=null,f=a,p=!1}}var ee=!1,S=null,te=-1,C=5,w=-1;function ne(){return!(e.unstable_now()-we||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(_(te),te=-1):h=!0,ce(b,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,se(x))),r},e.unstable_shouldYield=ne,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 ee(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{he=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:``)?me(e):``}function _e(e){switch(e.tag){case 5:return me(e.type);case 16:return me(`Lazy`);case 13:return me(`Suspense`);case 19:return me(`SuspenseList`);case 0:case 2:case 15:return e=ge(e.type,!1),e;case 11:return e=ge(e.type.render,!1),e;case 1:return e=ge(e.type,!0),e;default:return``}}function ve(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 w:return`Fragment`;case C:return`Portal`;case re:return`Profiler`;case ne:return`StrictMode`;case se:return`Suspense`;case ce:return`SuspenseList`}if(typeof e==`object`)switch(e.$$typeof){case ae:return(e.displayName||`Context`)+`.Consumer`;case ie:return(e._context.displayName||`Context`)+`.Provider`;case oe:var t=e.render;return e=e.displayName,e||=(e=t.displayName||t.name||``,e===``?`ForwardRef`:`ForwardRef(`+e+`)`),e;case T:return t=e.displayName||null,t===null?ve(e.type)||`Memo`:t;case le:t=e._payload,e=e._init;try{return ve(e(t))}catch{}}return null}function ye(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 ve(t);case 8:return t===ne?`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 D(e){switch(typeof e){case`boolean`:case`number`:case`string`:case`undefined`:return e;case`object`:return e;default:return``}}function be(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()===`input`&&(t===`checkbox`||t===`radio`)}function xe(e){var t=be(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 Se(e){e._valueTracker||=xe(e)}function Ce(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r=``;return e&&(r=be(e)?e.checked?`true`:`false`:e.value),e=r,e===n?!1:(t.setValue(e),!0)}function we(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 Te(e,t){var n=t.checked;return E({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Ee(e,t){var n=t.defaultValue==null?``:t.defaultValue,r=t.checked==null?t.defaultChecked:t.checked;n=D(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 De(e,t){t=t.checked,t!=null&&ee(e,`checked`,t,!1)}function O(e,t){De(e,t);var n=D(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`)?ke(e,t.type,n):t.hasOwnProperty(`defaultValue`)&&ke(e,t.type,D(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Oe(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 ke(e,t,n){(t!==`number`||we(e.ownerDocument)!==e)&&(n==null?e.defaultValue=``+e._wrapperState.initialValue:e.defaultValue!==``+n&&(e.defaultValue=``+n))}var Ae=Array.isArray;function je(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i`+t.valueOf().toString()+``,t=Le.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function ze(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Be={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},Ve=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(Be).forEach(function(e){Ve.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Be[t]=Be[e]})});function He(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||Be.hasOwnProperty(e)&&Be[e]?(``+t).trim():t+`px`}function Ue(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=He(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var We=E({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 Ge(e,t){if(t){if(We[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 Ke(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 qe=null;function Je(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Ye=null,Xe=null,Ze=null;function Qe(e){if(e=Ki(e)){if(typeof Ye!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Ji(t),Ye(e.stateNode,e.type,t))}}function $e(e){Xe?Ze?Ze.push(e):Ze=[e]:Xe=e}function et(){if(Xe){var e=Xe,t=Ze;if(Ze=Xe=null,Qe(e),t)for(e=0;e>>=0,e===0?32:31-(Mt(e)/Nt|0)|0}var Ft=64,It=4194304;function Lt(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 Rt(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=Lt(a))):r=Lt(s)}else o=n&~i,o===0?a!==0&&(r=Lt(a)):r=Lt(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 Wt(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-jt(t),e[t]=n}function Gt(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=or),lr=` `,ur=!1;function dr(e,t){switch(e){case`keyup`:return ir.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function fr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var pr=!1;function mr(e,t){switch(e){case`compositionend`:return fr(t);case`keypress`:return t.which===32?(ur=!0,lr):null;case`textInput`:return e=t.data,e===lr&&ur?null:e;default:return null}}function hr(e,t){if(pr)return e===`compositionend`||!ar&&dr(e,t)?(e=kn(),On=Dn=En=null,pr=!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=Lr(n)}}function zr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?zr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Br(){for(var e=window,t=we();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=we(e.document)}return t}function Vr(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 Hr(e){var t=Br(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&zr(n.ownerDocument.documentElement,n)){if(r!==null&&Vr(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=Rr(n,a);var o=Rr(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,Wr=null,Gr=null,Kr=null,qr=!1;function Jr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;qr||Wr==null||Wr!==we(r)||(r=Wr,`selectionStart`in r&&Vr(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&&Ir(Kr,r)||(Kr=r,r=yi(Gr,`onSelect`),0Xi||(e.current=Yi[Xi],Yi[Xi]=null,Xi--)}function R(e,t){Xi++,Yi[Xi]=e.current,e.current=t}var Qi={},z=Zi(Qi),$i=Zi(!1),ea=Qi;function ta(e,t){var n=e.type.contextTypes;if(!n)return Qi;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 na(e){return e=e.childContextTypes,e!=null}function ra(){L($i),L(z)}function ia(e,t,n){if(z.current!==Qi)throw Error(r(168));R(z,t),R($i,n)}function aa(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,ye(e)||`Unknown`,a));return E({},n,i)}function oa(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Qi,ea=z.current,R(z,e),R($i,$i.current),!0}function sa(e,t,n){var i=e.stateNode;if(!i)throw Error(r(169));n?(e=aa(e,t,ea),i.__reactInternalMemoizedMergedChildContext=e,L($i),L(z),R(z,e)):L($i),R($i,n)}var ca=null,la=!1,ua=!1;function da(e){ca===null?ca=[e]:ca.push(e)}function fa(e){la=!0,da(e)}function pa(){if(!ua&&ca!==null){ua=!0;var e=0,t=F;try{var n=ca;for(F=1;e>=o,i-=o,xa=1<<32-jt(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),B&&Ca(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),B&&Ca(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 B&&Ca(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)}),B&&Ca(a,g),u}function _(e,r,i,o){if(typeof i==`object`&&i&&i.type===w&&i.key===null&&(i=i.props.children),typeof i==`object`&&i){switch(i.$$typeof){case te:a:{for(var c=i.key,l=r;l!==null;){if(l.key===c){if(c=i.type,c===w){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===le&&Ha(c)===l.type){n(e,l.sibling),r=a(l,i.props),r.ref=Ba(e,l,i),r.return=e,e=r;break a}n(e,l);break}else t(e,l);l=l.sibling}i.type===w?(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=Ba(e,r,i),o.return=e,e=o)}return s(e);case C: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 le:return l=i._init,_(e,r,l(i._payload),o)}if(Ae(i))return h(e,r,i,o);if(fe(i))return g(e,r,i,o);Va(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 Wa=Ua(!0),Ga=Ua(!1),Ka=Zi(null),qa=null,Ja=null,Ya=null;function Xa(){Ya=Ja=qa=null}function Za(e){var t=Ka.current;L(Ka),e._currentValue=t}function Qa(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 $a(e,t){qa=e,Ya=Ja=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Rs=!0),e.firstContext=null)}function eo(e){var t=e._currentValue;if(Ya!==e)if(e={context:e,memoizedValue:t,next:null},Ja===null){if(qa===null)throw Error(r(308));Ja=e,qa.dependencies={lanes:0,firstContext:e}}else Ja=Ja.next=e;return t}var to=null;function no(e){to===null?to=[e]:to.push(e)}function ro(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,no(t)):(n.next=i.next,i.next=n),t.interleaved=n,io(e,r)}function io(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 ao=!1;function oo(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function so(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 co(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function lo(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,J&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,io(e,n)}return i=r.interleaved,i===null?(t.next=t,no(r)):(t.next=i.next,i.next=t),r.interleaved=t,io(e,n)}function uo(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,Kt(e,n)}}function fo(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 po(e,t,n,r){var i=e.updateQueue;ao=!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=E({},d,f);break a;case 2:ao=!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);Yc|=o,e.lanes=o,e.memoizedState=d}}function mo(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Oo.transition;Oo.transition={};try{e(!1),t()}finally{F=n,Oo.transition=r}}function ds(){return Ro().memoizedState}function fs(e,t,n){var r=ml(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},ms(e))hs(t,n);else if(n=ro(e,t,n,r),n!==null){var i=pl();hl(n,e,r,i),gs(n,t,r)}}function ps(e,t,n){var r=ml(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(ms(e))hs(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,Fr(s,o)){var c=t.interleaved;c===null?(i.next=i,no(t)):(i.next=c.next,c.next=i),t.interleaved=i;return}}catch{}n=ro(e,t,i,r),n!==null&&(i=pl(),hl(n,e,r,i),gs(n,t,r))}}function ms(e){var t=e.alternate;return e===H||t!==null&&t===H}function hs(e,t){jo=Ao=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function gs(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Kt(e,n)}}var _s={readContext:eo,useCallback:G,useContext:G,useEffect:G,useImperativeHandle:G,useInsertionEffect:G,useLayoutEffect:G,useMemo:G,useReducer:G,useRef:G,useState:G,useDebugValue:G,useDeferredValue:G,useTransition:G,useMutableSource:G,useSyncExternalStore:G,useId:G,unstable_isNewReconciler:!1},vs={readContext:eo,useCallback:function(e,t){return Lo().memoizedState=[e,t===void 0?null:t],e},useContext:eo,useEffect:es,useImperativeHandle:function(e,t,n){return n=n==null?null:n.concat([e]),Qo(4194308,4,is.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Qo(4194308,4,e,t)},useInsertionEffect:function(e,t){return Qo(4,2,e,t)},useMemo:function(e,t){var n=Lo();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Lo();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=fs.bind(null,H,e),[r.memoizedState,e]},useRef:function(e){var t=Lo();return e={current:e},t.memoizedState=e},useState:Yo,useDebugValue:os,useDeferredValue:function(e){return Lo().memoizedState=e},useTransition:function(){var e=Yo(!1),t=e[0];return e=us.bind(null,e[1]),Lo().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var i=H,a=Lo();if(B){if(n===void 0)throw Error(r(407));n=n()}else{if(n=t(),Y===null)throw Error(r(349));ko&30||Wo(i,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,es(Ko.bind(null,i,o,e),[e]),i.flags|=2048,Xo(9,Go.bind(null,i,o,n,t),void 0,null),n},useId:function(){var e=Lo(),t=Y.identifierPrefix;if(B){var n=Sa,r=xa;n=(r&~(1<<32-jt(r)-1)).toString(32)+n,t=`:`+t+`R`+n,n=Mo++,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[zi]=t,e[Bi]=i,cc(e,t,!1,!1),t.stateNode=e;a:{switch(c=Ke(n,i),n){case`dialog`:I(`cancel`,e),I(`close`,e),o=i;break;case`iframe`:case`object`:case`embed`:I(`load`,e),o=i;break;case`video`:case`audio`:for(o=0;otl&&(t.flags|=128,i=!0,dc(s,!1),t.lanes=4194304)}else{if(!i)if(e=wo(c),e!==null){if(t.flags|=128,i=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),dc(s,!0),s.tail===null&&s.tailMode===`hidden`&&!c.alternate&&!B)return fc(t),null}else 2*P()-s.renderingStartTime>tl&&n!==1073741824&&(t.flags|=128,i=!0,dc(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?(fc(t),null):(t=s.tail,s.rendering=t,s.tail=t.sibling,s.renderingStartTime=P(),t.sibling=null,n=V.current,R(V,i?n&1|2:n&1),t);case 22:case 23:return Tl(),i=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==i&&(t.flags|=8192),i&&t.mode&1?Kc&1073741824&&(fc(t),t.subtreeFlags&6&&(t.flags|=8192)):fc(t),null;case 24:return null;case 25:return null}throw Error(r(156,t.tag))}function mc(e,t){switch(Ea(t),t.tag){case 1:return na(t.type)&&ra(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return xo(),L($i),L(z),Eo(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Co(t),null;case 13:if(L(V),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(r(340));La()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return L(V),null;case 4:return xo(),null;case 10:return Za(t.type._context),null;case 22:case 23:return Tl(),null;case 24:return null;default:return null}}var hc=!1,gc=!1,_c=typeof WeakSet==`function`?WeakSet:Set,K=null;function vc(e,t){var n=e.ref;if(n!==null)if(typeof n==`function`)try{n(null)}catch(n){$(e,t,n)}else n.current=null}function yc(e,t,n){try{n()}catch(n){$(e,t,n)}}var bc=!1;function xc(e,t){if(Di=yn,e=Br(),Vr(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(Oi={focusedElem:e,selectionRange:n},yn=!1,K=t;K!==null;)if(t=K,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,K=e;else for(;K!==null;){t=K;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:xs(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){$(t,t.return,e)}if(e=t.sibling,e!==null){e.return=t.return,K=e;break}K=t.return}return h=bc,bc=!1,h}function Sc(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&&yc(t,n,a)}i=i.next}while(i!==r)}}function Cc(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 wc(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 Tc(e){var t=e.alternate;t!==null&&(e.alternate=null,Tc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[zi],delete t[Bi],delete t[Hi],delete t[Ui],delete t[Wi])),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 Ec(e){return e.tag===5||e.tag===3||e.tag===4}function Dc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Ec(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 Oc(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=Ei));else if(r!==4&&(e=e.child,e!==null))for(Oc(e,t,n),e=e.sibling;e!==null;)Oc(e,t,n),e=e.sibling}function kc(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(kc(e,t,n),e=e.sibling;e!==null;)kc(e,t,n),e=e.sibling}var q=null,Ac=!1;function jc(e,t,n){for(n=n.child;n!==null;)Mc(e,t,n),n=n.sibling}function Mc(e,t,n){if(kt&&typeof kt.onCommitFiberUnmount==`function`)try{kt.onCommitFiberUnmount(Ot,n)}catch{}switch(n.tag){case 5:gc||vc(n,t);case 6:var r=q,i=Ac;q=null,jc(e,t,n),q=r,Ac=i,q!==null&&(Ac?(e=q,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):q.removeChild(n.stateNode));break;case 18:q!==null&&(Ac?(e=q,n=n.stateNode,e.nodeType===8?Fi(e.parentNode,n):e.nodeType===1&&Fi(e,n),_n(e)):Fi(q,n.stateNode));break;case 4:r=q,i=Ac,q=n.stateNode.containerInfo,Ac=!0,jc(e,t,n),q=r,Ac=i;break;case 0:case 11:case 14:case 15:if(!gc&&(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)&&yc(n,t,o),i=i.next}while(i!==r)}jc(e,t,n);break;case 1:if(!gc&&(vc(n,t),r=n.stateNode,typeof r.componentWillUnmount==`function`))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(e){$(n,t,e)}jc(e,t,n);break;case 21:jc(e,t,n);break;case 22:n.mode&1?(gc=(r=gc)||n.memoizedState!==null,jc(e,t,n),gc=r):jc(e,t,n);break;default:jc(e,t,n)}}function Nc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new _c),t.forEach(function(t){var r=Hl.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))})}}function Pc(e,t){var n=t.deletions;if(n!==null)for(var i=0;ia&&(a=s),i&=~o}if(i=a,i=P()-i,i=(120>i?120:480>i?480:1080>i?1080:1920>i?1920:3e3>i?3e3:4320>i?4320:1960*Hc(i/1960))-i,10e?16:e,sl===null)var i=!1;else{if(e=sl,sl=null,cl=0,J&6)throw Error(r(331));var a=J;for(J|=4,K=e.current;K!==null;){var o=K,s=o.child;if(K.flags&16){var c=o.deletions;if(c!==null){for(var l=0;lP()-el?El(e,0):Zc|=n),gl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=It,It<<=1,!(It&130023424)&&(It=4194304)):t=1);var n=pl();e=io(e,t),e!==null&&(Wt(e,t,n),gl(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||$i.current)Rs=!0;else{if((e.lanes&n)===0&&!(t.flags&128))return Rs=!1,sc(e,t,n);Rs=!!(e.flags&131072)}else Rs=!1,B&&t.flags&1048576&&wa(t,_a,t.index);switch(t.lanes=0,t.tag){case 2:var i=t.type;ac(e,t),e=t.pendingProps;var a=ta(t,z.current);$a(t,n),a=Fo(null,t,i,e,a,n);var o=Io();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,na(i)?(o=!0,oa(t)):o=!1,t.memoizedState=a.state!==null&&a.state!==void 0?a.state:null,oo(t),a.updater=Cs,t.stateNode=a,a._reactInternals=t,Ds(t,i,e,n),t=qs(null,t,i,!0,o,n)):(t.tag=0,B&&o&&Ta(t),zs(null,t,a,n),t=t.child),t;case 16:i=t.elementType;a:{switch(ac(e,t),e=t.pendingProps,a=i._init,i=a(i._payload),t.type=i,a=t.tag=Jl(i),e=xs(i,e),a){case 0:t=Gs(null,t,i,e,n);break a;case 1:t=Ks(null,t,i,e,n);break a;case 11:t=Bs(null,t,i,e,n);break a;case 14:t=Vs(null,t,i,xs(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:xs(i,a),Gs(e,t,i,a,n);case 1:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:xs(i,a),Ks(e,t,i,a,n);case 3:a:{if(Js(t),e===null)throw Error(r(387));i=t.pendingProps,o=t.memoizedState,a=o.element,so(e,t),po(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=Os(Error(r(423)),t),t=Ys(e,t,i,n,a);break a}else if(i!==a){a=Os(Error(r(424)),t),t=Ys(e,t,i,n,a);break a}else for(Oa=Ii(t.stateNode.containerInfo.firstChild),Da=t,B=!0,ka=null,n=Ga(t,null,i,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(La(),i===a){t=oc(e,t,n);break a}zs(e,t,i,n)}t=t.child}return t;case 5:return So(t),e===null&&Na(t),i=t.type,a=t.pendingProps,o=e===null?null:e.memoizedProps,s=a.children,ki(i,a)?s=null:o!==null&&ki(i,o)&&(t.flags|=32),Ws(e,t),zs(e,t,s,n),t.child;case 6:return e===null&&Na(t),null;case 13:return Qs(e,t,n);case 4:return bo(t,t.stateNode.containerInfo),i=t.pendingProps,e===null?t.child=Wa(t,null,i,n):zs(e,t,i,n),t.child;case 11:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:xs(i,a),Bs(e,t,i,a,n);case 7:return zs(e,t,t.pendingProps,n),t.child;case 8:return zs(e,t,t.pendingProps.children,n),t.child;case 12:return zs(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,R(Ka,i._currentValue),i._currentValue=s,o!==null)if(Fr(o.value,s)){if(o.children===a.children&&!$i.current){t=oc(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=co(-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),Qa(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),Qa(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}zs(e,t,a.children,n),t=t.child}return t;case 9:return a=t.type,i=t.pendingProps.children,$a(t,n),a=eo(a),i=i(a),t.flags|=1,zs(e,t,i,n),t.child;case 14:return i=t.type,a=xs(i,t.pendingProps),a=xs(i.type,a),Vs(e,t,i,a,n);case 15:return Hs(e,t,t.type,t.pendingProps,n);case 17:return i=t.type,a=t.pendingProps,a=t.elementType===i?a:xs(i,a),ac(e,t),t.tag=1,na(i)?(e=!0,oa(t)):e=!1,$a(t,n),Ts(t,i,a),Ds(t,i,a,n),qs(null,t,i,!0,e,n);case 19:return ic(e,t,n);case 22:return Us(e,t,n)}throw Error(r(156,t.tag))};function Wl(e,t){return vt(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===oe)return 11;if(e===T)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 w:return Zl(n.children,a,o,t);case ne:s=8,a|=8;break;case re:return e=Kl(12,n,t,a|2),e.elementType=re,e.lanes=o,e;case se:return e=Kl(13,n,t,a),e.elementType=se,e.lanes=o,e;case ce:return e=Kl(19,n,t,a),e.elementType=ce,e.lanes=o,e;case ue:return Ql(n,a,o,t);default:if(typeof e==`object`&&e)switch(e.$$typeof){case ie:s=10;break a;case ae:s=9;break a;case oe:s=11;break a;case T:s=14;break a;case le: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=ue,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=Ut(0),this.expirationTimes=Ut(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ut(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},oo(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=await fetch(e,{credentials:`same-origin`,headers:{"Content-Type":`application/json`,...t.headers??{}},...t}),r=await n.text(),i=r?JSON.parse(r):null;if(!n.ok){let e=i?.error||i?.Error||i?.message||n.statusText;throw new v(n.status,e)}return i}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}`),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()}`),action:(e,t)=>y(e,{method:`POST`,body:JSON.stringify(t)})},ee=e=>e.replace(/([a-z0-9])([A-Z])/g,`$1-$2`).toLowerCase(),S=(...e)=>e.filter((e,t,n)=>!!e&&e.trim()!==``&&n.indexOf(e)===t).join(` `).trim(),te={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`},C=(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,...te,width:t,height:t,stroke:e,strokeWidth:r?Number(n)*24/Number(t):n,className:S(`lucide`,i),...s},[...o.map(([e,t])=>(0,g.createElement)(e,t)),...Array.isArray(a)?a:[a]])),w=(e,t)=>{let n=(0,g.forwardRef)(({className:n,...r},i)=>(0,g.createElement)(C,{ref:i,iconNode:t,className:S(`lucide-${ee(e)}`,n),...r}));return n.displayName=`${e}`,n},ne=w(`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`}]]),re=w(`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`}]]),ie=w(`CircleCheck`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m9 12 2 2 4-4`,key:`dzmm74`}]]),ae=w(`LoaderCircle`,[[`path`,{d:`M21 12a9 9 0 1 1-6.219-8.56`,key:`13zald`}]]),oe=w(`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`}]]),se=w(`ArrowLeft`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]),ce=w(`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`}]]),T=w(`Check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),le=w(`ChevronDown`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),ue=w(`ChevronRight`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),de=w(`Clock3`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`polyline`,{points:`12 6 12 12 16.5 12`,key:`1aq6pp`}]]),fe=w(`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`}]]),E=w(`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`}]]),pe=w(`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`}]]),me=w(`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`}]]),he=w(`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`}]]),ge=w(`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`}]]),_e=w(`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`}]]),ve=w(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),ye=w(`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`}]]),D=w(`Search`,[[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}],[`path`,{d:`m21 21-4.3-4.3`,key:`1qie3q`}]]),be=w(`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`}]]),xe=w(`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`}]]),Se=w(`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`}]]),Ce=w(`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`}]]),we=w(`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`}]]),Te=w(`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`}]]),Ee=w(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),De=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})),O=o(((e,t)=>{t.exports=De()}))(),Oe=`telesrv.admin.lang`,ke={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.dashboard":`Operations Console`,"route.dashboardSubtitle":`Console / Overview`,"route.messages":`Message Audit`,"route.messagesSubtitle":`Console / Messages`,"layout.navigation":`Navigation`,"layout.primaryNav":`Primary navigation`,"layout.dashboard":`Overview`,"layout.accounts":`Accounts`,"layout.channels":`Supergroups / Channels`,"layout.messages":`Messages`,"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":`中文`,"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`,"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`,"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.dashboard":`运维控制台`,"route.dashboardSubtitle":`控制台 / 总览`,"route.messages":`消息审计`,"route.messagesSubtitle":`控制台 / 消息`,"layout.navigation":`导航`,"layout.primaryNav":`主导航`,"layout.dashboard":`总览`,"layout.accounts":`账号`,"layout.channels":`超级群/频道`,"layout.messages":`消息`,"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":`中文`,"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":`频道/群`,"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":`频道帖子`,"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":`时间`}},Ae=(0,g.createContext)(null);function je({children:e}){let[t,n]=(0,g.useState)(()=>Pe());(0,g.useEffect)(()=>{try{localStorage.setItem(Oe,t)}catch{}document.documentElement.lang=t===`zh`?`zh-CN`:`en`,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=Ne(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>Ne(t,e,n)}),[t]);return(0,O.jsx)(Ae.Provider,{value:r,children:e})}function k(){let e=(0,g.useContext)(Ae);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function Me(){let{lang:e,setLang:t,t:n}=k();return(0,O.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`].map(r=>(0,O.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function Ne(e,t,n){let r=ke[e][t]??ke.en[t]??t;return n?r.replace(/\{(\w+)\}/g,(e,t)=>String(n[t]??``)):r}function Pe(){try{let e=Fe(new URLSearchParams(window.location.search).get(`lang`));if(e)return e}catch{}try{let e=Fe(localStorage.getItem(Oe));if(e)return e}catch{}let e=navigator.languages?.length?navigator.languages:[navigator.language];for(let t of e){let e=Fe(t);if(e)return e}return`en`}function Fe(e){if(!e)return null;let t=e.trim().toLowerCase().replace(`_`,`-`);return t===`zh`||t.startsWith(`zh-`)?`zh`:t===`en`||t.startsWith(`en-`)?`en`:null}function Ie(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function Le(e,t){return e.startsWith(`/accounts`)?t(`route.accounts`):e.startsWith(`/channels`)?t(`route.channels`):e.startsWith(`/messages`)?t(`route.messages`):t(`route.dashboard`)}function Re(e,t){return e.startsWith(`/accounts`)?t(`route.accountsSubtitle`):e.startsWith(`/channels`)?t(`route.channelsSubtitle`):e.startsWith(`/messages`)?t(`route.messagesSubtitle`):t(`route.dashboardSubtitle`)}function ze({href:e,navigate:t,className:n,children:r}){return(0,O.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function Be(){let{t:e}=k();return(0,O.jsxs)(`div`,{className:`boot-screen`,children:[(0,O.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,O.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,O.jsxs)(`span`,{children:[(0,O.jsx)(`strong`,{children:`telesrv`}),(0,O.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,O.jsx)(`div`,{className:`loader-bar`})]})}function Ve({actor:e,route:t,navigate:n,onLogout:r,children:i}){let{t:a}=k(),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,O.jsxs)(`div`,{className:`shell`,children:[(0,O.jsxs)(`aside`,{className:`sidebar`,children:[(0,O.jsxs)(ze,{className:`brand`,href:`/`,navigate:n,children:[(0,O.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,O.jsxs)(`span`,{children:[(0,O.jsx)(`strong`,{children:`telesrv`}),(0,O.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,O.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,O.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,O.jsx)(He,{icon:(0,O.jsx)(he,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,O.jsx)(He,{icon:(0,O.jsx)(Te,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,O.jsx)(He,{icon:(0,O.jsx)(xe,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,O.jsxs)(`div`,{className:`nav-section ${o?`active`:``} ${s?`open`:``}`,children:[(0,O.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":s,onClick:()=>c(e=>!e),children:[(0,O.jsx)(_e,{size:16}),(0,O.jsx)(`span`,{children:a(`layout.messages`)}),(0,O.jsx)(le,{className:`nav-section-chevron`,size:15})]}),s&&(0,O.jsxs)(`div`,{className:`nav-children`,children:[(0,O.jsx)(He,{href:`/messages/private`,route:t,navigate:n,activeWhen:e=>e===`/messages`||e===`/messages/detail`||e.startsWith(`/messages/private`),children:a(`layout.privateMessages`)}),(0,O.jsx)(He,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,O.jsxs)(`div`,{className:`sidebar-status`,children:[(0,O.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,O.jsxs)(`div`,{className:`runtime-row`,children:[(0,O.jsx)(be,{size:14}),(0,O.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,O.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,O.jsxs)(`div`,{className:`runtime-row`,children:[(0,O.jsx)(fe,{size:14}),(0,O.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,O.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,O.jsxs)(`div`,{className:`runtime-row`,children:[(0,O.jsx)(Se,{size:14}),(0,O.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,O.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,O.jsxs)(`div`,{className:`workspace`,children:[(0,O.jsxs)(`header`,{className:`topbar`,children:[(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`div`,{className:`eyebrow`,children:Re(t.path,a)}),(0,O.jsx)(`h1`,{children:Le(t.path,a)})]}),(0,O.jsxs)(`div`,{className:`topbar-actions`,children:[(0,O.jsx)(Me,{}),(0,O.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,O.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:l,title:a(`layout.logout`),children:[(0,O.jsx)(ge,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,O.jsx)(`main`,{className:`content`,children:i})]})]})}function He({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,O.jsxs)(ze,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,O.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,O.jsx)(`span`,{children:i})]})}function Ue(e){let t=e.trim();return!t||t.startsWith(`+`)?t:/^\d+$/.test(t)?`+${t}`:t}function We(e){let t=e.trim();return t?t.startsWith(`@`)?t:`@${t}`:``}function Ge(e){return`${e.FirstName||``} ${e.LastName||``}`.trim()||`-`}function Ke(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 qe(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function Je(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function Ye(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function Xe(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 Ze({title:e,eyebrow:t,children:n,actions:r}){return(0,O.jsxs)(`div`,{className:`page-frame`,children:[(0,O.jsxs)(`div`,{className:`page-title-row`,children:[(0,O.jsxs)(`div`,{children:[t&&(0,O.jsx)(`div`,{className:`eyebrow`,children:t}),(0,O.jsx)(`h2`,{children:e})]}),r&&(0,O.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Qe({children:e}){return(0,O.jsx)(`div`,{className:`query-panel`,children:e})}function $e({main:e,side:t}){return(0,O.jsxs)(`div`,{className:`split-layout`,children:[(0,O.jsx)(`div`,{className:`split-main`,children:e}),(0,O.jsx)(`aside`,{className:`split-side`,children:t})]})}function et({title:e,text:t,action:n}){return(0,O.jsxs)(`div`,{className:`section-head`,children:[(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`h2`,{children:e}),t&&(0,O.jsx)(`p`,{children:t})]}),n&&(0,O.jsx)(`div`,{className:`section-action`,children:n})]})}function tt({children:e}){return(0,O.jsxs)(`div`,{className:`alert`,children:[(0,O.jsx)(re,{size:16}),` `,(0,O.jsx)(`span`,{children:e})]})}function A({children:e,tone:t=`neutral`}){return(0,O.jsx)(`span`,{className:`badge ${t}`,children:e})}function nt({label:e,value:t,tone:n}){return(0,O.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,O.jsx)(`span`,{children:e}),(0,O.jsx)(`strong`,{children:t})]})}function j({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,O.jsxs)(`div`,{className:`metric ${n}`,children:[(0,O.jsx)(`span`,{children:e}),(0,O.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function M({label:e,value:t,mono:n=!1}){return(0,O.jsxs)(`div`,{className:`summary-item`,children:[(0,O.jsx)(`span`,{children:e}),(0,O.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function rt({rows:e}){let{t}=k();return(0,O.jsx)(`div`,{className:`table-wrap`,children:(0,O.jsxs)(`table`,{className:`data-table`,children:[(0,O.jsx)(`thead`,{children:(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`th`,{children:t(`audit.id`)}),(0,O.jsx)(`th`,{children:t(`audit.commandID`)}),(0,O.jsx)(`th`,{children:t(`audit.action`)}),(0,O.jsx)(`th`,{children:t(`audit.actor`)}),(0,O.jsx)(`th`,{children:t(`audit.status`)}),(0,O.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,O.jsx)(`th`,{children:t(`audit.reason`)}),(0,O.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,O.jsxs)(`tbody`,{children:[e.map(e=>(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`td`,{children:e.ID}),(0,O.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,O.jsx)(`td`,{children:e.Action}),(0,O.jsx)(`td`,{children:e.Actor}),(0,O.jsx)(`td`,{children:e.Status}),(0,O.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,O.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,O.jsx)(`td`,{children:qe(e.CreatedAt)})]},e.ID)),e.length===0&&(0,O.jsx)(it,{colSpan:8})]})]})})}function it({colSpan:e}){let{t}=k();return(0,O.jsx)(`tr`,{children:(0,O.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function at({label:e}){return(0,O.jsx)(`section`,{className:`surface`,children:(0,O.jsx)(`div`,{className:`loading-line`,children:e})})}function ot({value:e}){return(0,O.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function st({onLogin:e}){let{t}=k(),[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,O.jsx)(`main`,{className:`login-page`,children:(0,O.jsxs)(`section`,{className:`login-panel`,children:[(0,O.jsxs)(`div`,{className:`login-head`,children:[(0,O.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,O.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,O.jsxs)(`span`,{children:[(0,O.jsx)(`strong`,{children:`telesrv`}),(0,O.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,O.jsxs)(`div`,{className:`login-head-actions`,children:[(0,O.jsx)(Me,{}),(0,O.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,O.jsxs)(`div`,{className:`login-copy`,children:[(0,O.jsx)(`h1`,{children:t(`login.heading`)}),(0,O.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,O.jsx)(tt,{children:i}),(0,O.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,O.jsxs)(`label`,{children:[(0,O.jsx)(`span`,{children:t(`login.secret`)}),(0,O.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,O.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var ct=m();function N({label:e,path:t,payload:n,icon:r,compact:i=!1,tone:a=`danger`,onDone:o}){let{t:s}=k(),[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 ee(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 S=f?.dry_run&&!f.error,te=`btn ${a===`danger`?`danger`:a===`warn`?`warn`:``} ${i?`compact-btn`:``}`,C=(0,g.useMemo)(()=>{try{return n()}catch(e){return{payload_error:b(e)}}},[c,n]);return(0,O.jsxs)(O.Fragment,{children:[(0,O.jsxs)(`button`,{className:te,type:`button`,onClick:()=>{y(),l(!0)},children:[r,e]}),c&&(0,ct.createPortal)((0,O.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,O.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,O.jsxs)(`div`,{className:`modal-head`,children:[(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`div`,{className:`eyebrow`,children:s(`action.flow`)}),(0,O.jsx)(`h2`,{children:e})]}),(0,O.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>l(!1),"aria-label":s(`action.close`),children:(0,O.jsx)(Ee,{size:15})})]}),(0,O.jsxs)(`div`,{className:`command-body`,children:[(0,O.jsxs)(`div`,{className:`command-steps`,children:[(0,O.jsxs)(`div`,{className:`command-step ${u.trim()?`done`:`active`}`,children:[(0,O.jsx)(`span`,{children:`1`}),(0,O.jsx)(`strong`,{children:s(`action.stepReason`)})]}),(0,O.jsxs)(`div`,{className:`command-step ${f?.dry_run?`done`:u.trim()?`active`:``}`,children:[(0,O.jsx)(`span`,{children:`2`}),(0,O.jsx)(`strong`,{children:s(`action.stepDryRun`)})]}),(0,O.jsxs)(`div`,{className:`command-step ${f&&!f.dry_run&&!f.error?`done`:S?`active`:``}`,children:[(0,O.jsx)(`span`,{children:`3`}),(0,O.jsx)(`strong`,{children:s(`action.stepConfirm`)})]})]}),(0,O.jsxs)(`label`,{className:`form-field`,children:[(0,O.jsx)(`span`,{children:s(`action.reason`)}),(0,O.jsx)(`textarea`,{value:u,onChange:e=>d(e.target.value),rows:3,placeholder:s(`action.reasonPlaceholder`)})]}),(0,O.jsxs)(`div`,{className:`command-preview`,children:[(0,O.jsxs)(`div`,{className:`preview-head`,children:[(0,O.jsx)(E,{size:14}),` `,s(`action.requestPreview`)]}),(0,O.jsx)(ot,{value:JSON.stringify(C,null,2)})]}),m&&(0,O.jsx)(tt,{children:m}),f&&(0,O.jsxs)(`div`,{className:`result-box`,children:[(0,O.jsxs)(`div`,{className:`result-title`,children:[f.error?(0,O.jsx)(re,{size:16}):(0,O.jsx)(ie,{size:16}),(0,O.jsx)(`strong`,{children:f.message||f.error||s(`action.result`)})]}),(0,O.jsxs)(`div`,{className:`result-line`,children:[(0,O.jsx)(`span`,{children:s(`action.commandID`)}),(0,O.jsx)(`strong`,{children:f.command_id})]}),(0,O.jsxs)(`div`,{className:`result-line`,children:[(0,O.jsx)(`span`,{children:s(`action.status`)}),(0,O.jsx)(`strong`,{children:f.status})]}),(0,O.jsxs)(`div`,{className:`result-line`,children:[(0,O.jsx)(`span`,{children:s(`action.dryRun`)}),(0,O.jsx)(`strong`,{children:f.dry_run?s(`common.yes`):s(`common.no`)})]}),(0,O.jsx)(`div`,{className:`result-message`,children:f.message||f.error}),f.details&&(0,O.jsx)(ot,{value:JSON.stringify(f.details,null,2)})]})]}),(0,O.jsxs)(`div`,{className:`modal-actions`,children:[(0,O.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>l(!1),children:s(`common.close`)}),(0,O.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>ee(!1),disabled:_,children:[_?(0,O.jsx)(ae,{size:15,className:`spin`}):(0,O.jsx)(ve,{size:15}),s(f?`action.runAgain`:`action.runDry`)]}),(0,O.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>ee(!0),disabled:_||!S,children:[(0,O.jsx)(ie,{size:15}),s(`action.confirm`)]})]})]})}),document.body)]})}function lt({rows:e,userID:t,onDone:n}){let{t:r}=k(),[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,O.jsxs)(`div`,{className:`authorization-block`,children:[(0,O.jsx)(`div`,{className:`table-wrap`,children:(0,O.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,O.jsx)(`thead`,{children:(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`th`,{children:r(`auth.device`)}),(0,O.jsx)(`th`,{children:r(`auth.platform`)}),(0,O.jsx)(`th`,{children:r(`auth.ip`)}),(0,O.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,O.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,O.jsxs)(`tbody`,{children:[o.map(n=>(0,O.jsxs)(`tr`,{children:[(0,O.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,O.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,O.jsx)(`td`,{children:n.IP}),(0,O.jsx)(`td`,{children:qe(n.ActiveAt)}),(0,O.jsx)(`td`,{className:`device-actions-cell`,children:(0,O.jsxs)(`div`,{className:`device-actions`,children:[(0,O.jsx)(N,{label:r(`auth.revokeCurrent`),icon:(0,O.jsx)(ge,{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,O.jsx)(N,{label:r(`auth.keepCurrent`),icon:(0,O.jsx)(xe,{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,O.jsx)(it,{colSpan:5})]})]})}),(0,O.jsx)(`div`,{className:`danger-zone`,children:(0,O.jsx)(N,{label:r(`auth.revokeAll`),icon:(0,O.jsx)(ce,{size:15}),path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,revoke_all:!0}),onDone:()=>s(()=>new Set(e.map(e=>e.Hash)))})})]})}function ut({id:e,navigate:t}){let{t:n}=k(),[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)(()=>dt(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(dt(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,O.jsx)(tt,{children:a});if(!r)return(0,O.jsx)(at,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,O.jsx)(Ze,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,O.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,O.jsx)(se,{size:15}),` `,n(`common.backToList`)]}),children:(0,O.jsx)($e,{main:(0,O.jsxs)(`div`,{className:`stacked-sections`,children:[(0,O.jsxs)(`section`,{className:`entity-head`,children:[(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`div`,{className:`entity-title`,children:Ge(y)}),(0,O.jsxs)(`div`,{className:`entity-subtitle`,children:[We(y.Username)||n(`account.noUsername`),` · `,Ue(y.Phone)||n(`account.noPhone`)]})]}),(0,O.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,O.jsx)(A,{tone:`good`,children:n(`account.premium`)}):(0,O.jsx)(A,{children:n(`account.notPremium`)}),r.Verified?(0,O.jsx)(A,{tone:`good`,children:n(`common.verified`)}):(0,O.jsx)(A,{children:n(`account.notVerified`)}),y.Frozen?(0,O.jsx)(A,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,O.jsx)(A,{children:n(`account.accountActive`)})]})]}),(0,O.jsxs)(`div`,{className:`summary-grid`,children:[(0,O.jsx)(M,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,O.jsx)(M,{label:n(`account.lastActive`),value:Je(r.LastSeenAt)||`-`}),(0,O.jsx)(M,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?Je(y.PremiumUntil):n(`common.none`)}),(0,O.jsx)(M,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,O.jsx)(M,{label:n(`common.updatedAt`),value:qe(y.UpdatedAt)||`-`}),(0,O.jsx)(M,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,O.jsx)(M,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,O.jsx)(M,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,O.jsx)(M,{label:n(`account.freezeSince`),value:r.Restriction.Since?qe(r.Restriction.Since):n(`common.none`)}),(0,O.jsx)(M,{label:n(`account.freezeUntil`),value:r.Restriction.Until?qe(r.Restriction.Until):n(`common.none`)}),(0,O.jsx)(M,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,O.jsx)(M,{label:n(`account.createdAt`),value:qe(y.CreatedAt)||`-`})]}),r.About&&(0,O.jsx)(`p`,{className:`about-text`,children:r.About}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,O.jsx)(lt,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,O.jsx)(rt,{rows:r.AuditLogs})]})]}),side:(0,O.jsxs)(`section`,{className:`action-dock`,children:[(0,O.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,O.jsxs)(`label`,{className:`duration-field`,children:[(0,O.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,O.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,O.jsxs)(`label`,{className:`duration-field`,children:[(0,O.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,O.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,O.jsx)(N,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,O.jsx)(re,{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,O.jsx)(N,{label:n(`account.unfreezeAccount`),icon:(0,O.jsx)(re,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,O.jsxs)(`label`,{className:`duration-field`,children:[(0,O.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,O.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,O.jsxs)(`div`,{className:`action-stack`,children:[(0,O.jsx)(N,{label:n(`account.setPremium`),icon:(0,O.jsx)(oe,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:Ye(l)}),onDone:v}),(0,O.jsx)(N,{label:n(`account.clearPremium`),icon:(0,O.jsx)(oe,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,O.jsxs)(`label`,{className:`duration-field`,children:[(0,O.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,O.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,O.jsx)(N,{label:n(`account.grantStars`),icon:(0,O.jsx)(Ce,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:Ye(d)}),onDone:v}),(0,O.jsx)(N,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,O.jsx)(ne,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:y.ID,verified:!r.Verified}),onDone:v})]})]})})})}function dt(e){return new Date(e.getTime()-e.getTimezoneOffset()*6e4).toISOString().slice(0,16)}function ft(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 pt(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 mt({navigate:e}){let{t}=k(),[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=ft(o?.rows??[]);return(0,O.jsxs)(Ze,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,O.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,O.jsx)(ye,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,O.jsx)(tt,{children:f}),(0,O.jsxs)(`div`,{className:`metric-row`,children:[(0,O.jsx)(j,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,O.jsx)(j,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,O.jsx)(j,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,O.jsx)(j,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,O.jsx)(Qe,{children:(0,O.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,O.jsxs)(`label`,{className:`searchbox`,children:[(0,O.jsx)(D,{size:15}),(0,O.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,O.jsxs)(`label`,{className:`field-inline`,children:[(0,O.jsx)(`span`,{children:t(`common.limit`)}),(0,O.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,O.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,O.jsx)(ae,{size:15,className:`spin`}):(0,O.jsx)(D,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,O.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,O.jsx)(ue,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,O.jsx)(`div`,{className:`table-wrap`,children:(0,O.jsxs)(`table`,{className:`data-table`,children:[(0,O.jsx)(`thead`,{children:(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`th`,{children:t(`account.userID`)}),(0,O.jsx)(`th`,{children:t(`account.phone`)}),(0,O.jsx)(`th`,{children:t(`common.username`)}),(0,O.jsx)(`th`,{children:t(`common.name`)}),(0,O.jsx)(`th`,{children:t(`common.device`)}),(0,O.jsx)(`th`,{children:t(`account.lastActive`)}),(0,O.jsx)(`th`,{children:t(`account.premium`)}),(0,O.jsx)(`th`,{children:t(`common.verified`)}),(0,O.jsx)(`th`,{children:t(`account.frozen`)}),(0,O.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,O.jsx)(`th`,{})]})}),(0,O.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`td`,{className:`mono`,children:n.ID}),(0,O.jsx)(`td`,{children:Ue(n.Phone)}),(0,O.jsx)(`td`,{children:We(n.Username)}),(0,O.jsx)(`td`,{children:Ge(n)}),(0,O.jsx)(`td`,{children:n.DeviceCount}),(0,O.jsx)(`td`,{children:qe(n.LastActiveAt)}),(0,O.jsx)(`td`,{children:n.PremiumUntil>0?(0,O.jsxs)(A,{tone:`good`,children:[t(`account.premium`),` `,Je(n.PremiumUntil)]}):(0,O.jsx)(A,{children:t(`common.none`)})}),(0,O.jsx)(`td`,{children:n.Verified?(0,O.jsx)(A,{tone:`good`,children:t(`common.verified`)}):(0,O.jsx)(A,{children:t(`account.notVerified`)})}),(0,O.jsx)(`td`,{children:n.Frozen?(0,O.jsx)(A,{tone:`danger`,children:t(`account.frozen`)}):(0,O.jsx)(A,{children:t(`common.normal`)})}),(0,O.jsx)(`td`,{children:qe(n.UpdatedAt)}),(0,O.jsx)(`td`,{children:(0,O.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,O.jsx)(ue,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,O.jsx)(it,{colSpan:11})]})]})})]})}function ht({id:e,navigate:t}){let{t:n}=k(),[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,O.jsx)(tt,{children:a});if(!r)return(0,O.jsx)(at,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,O.jsx)(Ze,{title:`${Ke(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,O.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,O.jsx)(se,{size:15}),` `,n(`common.backToList`)]}),children:(0,O.jsx)($e,{main:(0,O.jsxs)(`div`,{className:`stacked-sections`,children:[(0,O.jsxs)(`section`,{className:`entity-head`,children:[(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,O.jsxs)(`div`,{className:`entity-subtitle`,children:[We(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,O.jsxs)(`div`,{className:`entity-badges`,children:[(0,O.jsx)(A,{children:Ke(c,n)}),c.Verified?(0,O.jsx)(A,{tone:`good`,children:n(`common.verified`)}):(0,O.jsx)(A,{children:n(`account.notVerified`)}),c.Deleted?(0,O.jsx)(A,{tone:`danger`,children:n(`common.deleted`)}):(0,O.jsx)(A,{children:n(`common.valid`)})]})]}),(0,O.jsxs)(`div`,{className:`summary-grid`,children:[(0,O.jsx)(M,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,O.jsx)(M,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,O.jsx)(M,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,O.jsx)(M,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,O.jsx)(M,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,O.jsx)(M,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,O.jsx)(M,{label:n(`account.createdAt`),value:Je(c.Date)||`-`}),(0,O.jsx)(M,{label:n(`common.updatedAt`),value:qe(c.UpdatedAt)||`-`})]}),c.About&&(0,O.jsx)(`p`,{className:`about-text`,children:c.About}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,O.jsx)(rt,{rows:r.AuditLogs})]}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,O.jsx)(ot,{value:r.ChannelJSON})]})]}),side:(0,O.jsxs)(`section`,{className:`action-dock`,children:[(0,O.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,O.jsx)(N,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,O.jsx)(ne,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:c.ID,verified:!c.Verified}),onDone:s})]})})})}function gt({navigate:e}){let{t}=k(),[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=pt(o?.rows??[]);return(0,O.jsxs)(Ze,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,O.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,O.jsx)(ye,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,O.jsx)(tt,{children:f}),(0,O.jsxs)(`div`,{className:`metric-row`,children:[(0,O.jsx)(j,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,O.jsx)(j,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,O.jsx)(j,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,O.jsx)(j,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,O.jsx)(Qe,{children:(0,O.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,O.jsxs)(`label`,{className:`searchbox`,children:[(0,O.jsx)(D,{size:15}),(0,O.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,O.jsxs)(`label`,{className:`field-inline`,children:[(0,O.jsx)(`span`,{children:t(`common.limit`)}),(0,O.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,O.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,O.jsx)(ae,{size:15,className:`spin`}):(0,O.jsx)(D,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,O.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,O.jsx)(ue,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,O.jsx)(`div`,{className:`table-wrap`,children:(0,O.jsxs)(`table`,{className:`data-table`,children:[(0,O.jsx)(`thead`,{children:(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`th`,{children:t(`channel.channelID`)}),(0,O.jsx)(`th`,{children:t(`channel.kind`)}),(0,O.jsx)(`th`,{children:t(`common.username`)}),(0,O.jsx)(`th`,{children:t(`channel.title`)}),(0,O.jsx)(`th`,{children:t(`common.members`)}),(0,O.jsx)(`th`,{children:t(`common.admins`)}),(0,O.jsx)(`th`,{children:`PTS`}),(0,O.jsx)(`th`,{children:t(`common.verified`)}),(0,O.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,O.jsx)(`th`,{})]})}),(0,O.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`td`,{className:`mono`,children:n.ID}),(0,O.jsx)(`td`,{children:Ke(n,t)}),(0,O.jsx)(`td`,{children:We(n.Username)}),(0,O.jsx)(`td`,{children:n.Title}),(0,O.jsx)(`td`,{children:n.ParticipantsCount}),(0,O.jsx)(`td`,{children:n.AdminsCount}),(0,O.jsx)(`td`,{children:n.PTS}),(0,O.jsx)(`td`,{children:n.Verified?(0,O.jsx)(A,{tone:`good`,children:t(`common.verified`)}):(0,O.jsx)(A,{children:t(`account.notVerified`)})}),(0,O.jsx)(`td`,{children:qe(n.UpdatedAt)}),(0,O.jsx)(`td`,{children:(0,O.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,O.jsx)(ue,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,O.jsx)(it,{colSpan:10})]})]})})]})}function _t({navigate:e}){let{t}=k();return(0,O.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,O.jsxs)(`section`,{className:`overview-band`,children:[(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,O.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,O.jsxs)(`div`,{className:`overview-metrics`,children:[(0,O.jsx)(nt,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,O.jsx)(nt,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,O.jsx)(nt,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,O.jsxs)(`div`,{className:`command-grid`,children:[(0,O.jsx)(vt,{icon:(0,O.jsx)(Te,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,O.jsx)(vt,{icon:(0,O.jsx)(xe,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,O.jsx)(vt,{icon:(0,O.jsx)(_e,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,O.jsxs)(`section`,{className:`work-strip`,children:[(0,O.jsxs)(`div`,{className:`strip-item`,children:[(0,O.jsx)(ie,{size:16}),(0,O.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,O.jsxs)(`div`,{className:`strip-item`,children:[(0,O.jsx)(me,{size:16}),(0,O.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,O.jsxs)(`div`,{className:`strip-item`,children:[(0,O.jsx)(de,{size:16}),(0,O.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,O.jsxs)(`div`,{className:`strip-item`,children:[(0,O.jsx)(E,{size:16}),(0,O.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function vt({icon:e,title:t,text:n,href:r,navigate:i}){return(0,O.jsxs)(ze,{className:`launcher`,href:r,navigate:i,children:[(0,O.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,O.jsxs)(`span`,{className:`launcher-copy`,children:[(0,O.jsx)(`strong`,{children:t}),(0,O.jsx)(`span`,{children:n})]}),(0,O.jsx)(ue,{size:16})]})}function yt({channelID:e,msgID:t,navigate:n}){let{t:r}=k(),[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,O.jsx)(tt,{children:o});if(!i)return(0,O.jsx)(at,{label:r(`common.loading`)});let l=i.Message;return(0,O.jsx)(Ze,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,O.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,O.jsx)(se,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,O.jsxs)(`div`,{className:`stacked-sections`,children:[(0,O.jsxs)(`section`,{className:`entity-head`,children:[(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,O.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:Je(l.Date)})})]}),(0,O.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,O.jsx)(A,{tone:`danger`,children:r(`common.deleted`)}):(0,O.jsx)(A,{children:r(`common.survived`)}),l.Pinned&&(0,O.jsx)(A,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,O.jsx)(A,{children:r(`messages.channelPost`)}),(0,O.jsxs)(A,{children:[`pts `,l.PTS]})]})]}),(0,O.jsxs)(`div`,{className:`summary-grid`,children:[(0,O.jsx)(M,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,O.jsx)(M,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,O.jsx)(M,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,O.jsx)(M,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,O.jsx)(ot,{value:i.MessageJSON})]}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,O.jsx)(ot,{value:i.ChannelJSON})]}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,O.jsx)(`div`,{className:`table-wrap`,children:(0,O.jsxs)(`table`,{className:`data-table`,children:[(0,O.jsx)(`thead`,{children:(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`th`,{children:`PTS`}),(0,O.jsx)(`th`,{children:r(`common.count`)}),(0,O.jsx)(`th`,{children:r(`common.type`)}),(0,O.jsx)(`th`,{children:r(`common.messageId`)}),(0,O.jsx)(`th`,{children:r(`common.sender`)}),(0,O.jsx)(`th`,{children:r(`common.time`)})]})}),(0,O.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`td`,{children:e.PTS}),(0,O.jsx)(`td`,{children:e.PTSCount}),(0,O.jsx)(`td`,{children:e.Type}),(0,O.jsx)(`td`,{children:e.MessageID}),(0,O.jsx)(`td`,{children:e.SenderUserID}),(0,O.jsx)(`td`,{children:Je(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,O.jsx)(it,{colSpan:6})]})]})})]}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:r(`messages.eventJson`)}),(0,O.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,O.jsx)(ot,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,O.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function bt({label:e,value:t,onChange:n}){let{t:r}=k(),[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,O.jsxs)(`div`,{className:`entity-picker`,children:[(0,O.jsxs)(`div`,{className:`picker-head`,children:[(0,O.jsx)(`span`,{children:e}),t?(0,O.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,O.jsx)(Ee,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,O.jsxs)(`div`,{className:`selected-entity`,children:[(0,O.jsx)(T,{size:15}),(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`strong`,{children:Ge(t)}),(0,O.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,O.jsx)(`span`,{children:We(t.Username)||Ue(t.Phone)||`-`})]}):null,(0,O.jsxs)(`div`,{className:`picker-search`,children:[(0,O.jsx)(D,{size:15}),(0,O.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,O.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,O.jsx)(ae,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,O.jsx)(`div`,{className:`picker-error`,children:u}),(0,O.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,O.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,O.jsx)(`span`,{className:`mono`,children:e.ID}),(0,O.jsx)(`strong`,{children:Ge(e)}),(0,O.jsx)(`span`,{children:We(e.Username)||Ue(e.Phone)||`-`}),e.Verified?(0,O.jsx)(A,{tone:`good`,children:r(`picker.verified`)}):(0,O.jsx)(A,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,O.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function xt({label:e,value:t,onChange:n}){let{t:r}=k(),[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,O.jsxs)(`div`,{className:`entity-picker`,children:[(0,O.jsxs)(`div`,{className:`picker-head`,children:[(0,O.jsx)(`span`,{children:e}),t?(0,O.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,O.jsx)(Ee,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,O.jsxs)(`div`,{className:`selected-entity`,children:[(0,O.jsx)(T,{size:15}),(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`strong`,{children:t.Title||`-`}),(0,O.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,O.jsx)(`span`,{children:We(t.Username)||Ke(t,r)})]}):null,(0,O.jsxs)(`div`,{className:`picker-search`,children:[(0,O.jsx)(D,{size:15}),(0,O.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,O.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,O.jsx)(ae,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,O.jsx)(`div`,{className:`picker-error`,children:u}),(0,O.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,O.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,O.jsx)(`span`,{className:`mono`,children:e.ID}),(0,O.jsx)(`strong`,{children:e.Title||`-`}),(0,O.jsx)(`span`,{children:We(e.Username)||Ke(e,r)}),e.Verified?(0,O.jsx)(A,{tone:`good`,children:r(`picker.verified`)}):(0,O.jsx)(A,{children:Ke(e,r)})]},e.ID)),o.length===0&&!c?(0,O.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function P({navigate:e}){let{t}=k(),[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,O.jsxs)(Ze,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,O.jsx)(tt,{children:f}),(0,O.jsxs)(Qe,{children:[(0,O.jsx)(`div`,{className:`message-selector-grid single`,children:(0,O.jsx)(xt,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,O.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,O.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,O.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,O.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,O.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,O.jsx)(D,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,O.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,O.jsx)(ue,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,O.jsxs)(`div`,{className:`metric-row`,children:[(0,O.jsx)(j,{label:t(`messages.currentPage`),value:String(_.length)}),(0,O.jsx)(j,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,O.jsx)(j,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,O.jsx)(j,{label:t(`messages.channelGroup`),value:n?`${n.Title||Ke(n,t)} (${n.ID})`:`-`})]}),(0,O.jsx)(`div`,{className:`table-wrap`,children:(0,O.jsxs)(`table`,{className:`data-table`,children:[(0,O.jsx)(`thead`,{children:(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`th`,{children:t(`common.messageId`)}),(0,O.jsx)(`th`,{children:t(`common.time`)}),(0,O.jsx)(`th`,{children:t(`common.sender`)}),(0,O.jsx)(`th`,{children:`From Peer`}),(0,O.jsx)(`th`,{children:`PTS`}),(0,O.jsx)(`th`,{children:t(`common.views`)}),(0,O.jsx)(`th`,{children:t(`common.status`)}),(0,O.jsx)(`th`,{children:t(`messages.body`)}),(0,O.jsx)(`th`,{})]})}),(0,O.jsxs)(`tbody`,{children:[_.map(n=>(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`td`,{className:`mono`,children:n.ID}),(0,O.jsx)(`td`,{children:Je(n.Date)}),(0,O.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,O.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,O.jsx)(`td`,{children:n.PTS}),(0,O.jsx)(`td`,{children:n.ViewsCount}),(0,O.jsx)(`td`,{children:n.Deleted?(0,O.jsx)(A,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,O.jsx)(A,{tone:`warn`,children:t(`messages.pinned`)}):(0,O.jsx)(A,{children:t(`common.survived`)})}),(0,O.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,O.jsx)(`td`,{children:(0,O.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,O.jsx)(ue,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,O.jsx)(it,{colSpan:9})]})]})})]})}function St({ownerUserID:e,msgID:t,navigate:n}){let{t:r}=k(),[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,O.jsx)(tt,{children:o});if(!i)return(0,O.jsx)(at,{label:r(`common.loading`)});let l=i.Message;return(0,O.jsx)(Ze,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,O.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,O.jsx)(se,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,O.jsx)($e,{main:(0,O.jsxs)(`div`,{className:`stacked-sections`,children:[(0,O.jsxs)(`section`,{className:`entity-head`,children:[(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,O.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:Je(l.Date)})})]}),(0,O.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,O.jsx)(A,{tone:`danger`,children:r(`common.deleted`)}):(0,O.jsx)(A,{children:r(`common.survived`)}),(0,O.jsxs)(A,{children:[`pts `,l.PTS]}),(0,O.jsx)(A,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,O.jsxs)(`div`,{className:`summary-grid`,children:[(0,O.jsx)(M,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,O.jsx)(M,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,O.jsx)(M,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,O.jsx)(M,{label:r(`common.time`),value:Je(l.Date)})]}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,O.jsx)(ot,{value:i.MessageJSON})]}),(0,O.jsxs)(`div`,{className:`raw-grid`,children:[(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,O.jsx)(ot,{value:i.DialogJSON})]}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,O.jsx)(ot,{value:i.PrivateJSON})]})]}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,O.jsx)(`div`,{className:`table-wrap`,children:(0,O.jsxs)(`table`,{className:`data-table`,children:[(0,O.jsx)(`thead`,{children:(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`th`,{children:`PTS`}),(0,O.jsx)(`th`,{children:r(`common.count`)}),(0,O.jsx)(`th`,{children:r(`common.type`)}),(0,O.jsx)(`th`,{children:r(`common.time`)})]})}),(0,O.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`td`,{children:e.PTS}),(0,O.jsx)(`td`,{children:e.PTSCount}),(0,O.jsx)(`td`,{children:e.Type}),(0,O.jsx)(`td`,{children:Je(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,O.jsx)(it,{colSpan:4})]})]})})]}),(0,O.jsxs)(`section`,{className:`section-block`,children:[(0,O.jsx)(et,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,O.jsx)(`div`,{className:`table-wrap`,children:(0,O.jsxs)(`table`,{className:`data-table`,children:[(0,O.jsx)(`thead`,{children:(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`th`,{children:`ID`}),(0,O.jsx)(`th`,{children:r(`account.userID`)}),(0,O.jsx)(`th`,{children:`PTS`}),(0,O.jsx)(`th`,{children:r(`common.type`)}),(0,O.jsx)(`th`,{children:r(`common.status`)}),(0,O.jsx)(`th`,{children:r(`messages.attempts`)}),(0,O.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,O.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`td`,{children:e.ID}),(0,O.jsx)(`td`,{children:e.TargetUserID}),(0,O.jsx)(`td`,{children:e.PTS}),(0,O.jsx)(`td`,{children:e.EventType}),(0,O.jsx)(`td`,{children:e.Status}),(0,O.jsx)(`td`,{children:e.Attempts}),(0,O.jsx)(`td`,{children:qe(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,O.jsx)(it,{colSpan:7})]})]})})]})]}),side:(0,O.jsxs)(`section`,{className:`action-dock`,children:[(0,O.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,O.jsx)(N,{label:r(`messages.deleteThis`),icon:(0,O.jsx)(we,{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 Ct({navigate:e}){let{t}=k(),[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,ee]=(0,g.useState)(``),[S,te]=(0,g.useState)(`1`),[C,w]=(0,g.useState)(null),[ne,re]=(0,g.useState)(``);async function ie(e=!1){if(re(``),!n||!i){re(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 x.messages(r))}catch(e){re(b(e))}}function ae(e){r(e),s(``),l(``),w(null)}function oe(e){a(e),s(``),l(``),w(null)}return(0,O.jsxs)(Ze,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[ne&&(0,O.jsx)(tt,{children:ne}),(0,O.jsxs)(Qe,{children:[(0,O.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,O.jsx)(bt,{label:t(`messages.ownerUser`),value:n,onChange:ae}),(0,O.jsx)(bt,{label:t(`messages.peerUser`),value:i,onChange:oe})]}),(0,O.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),ie(!1)},children:[(0,O.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,O.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,O.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,O.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,O.jsx)(D,{size:15}),` `,t(`messages.searchMessages`)]}),C?.rows.length?(0,O.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>ie(!0),children:[(0,O.jsx)(ue,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,O.jsxs)(`div`,{className:`metric-row`,children:[(0,O.jsx)(j,{label:t(`messages.currentPage`),value:String(C?.rows.length??0)}),(0,O.jsx)(j,{label:t(`messages.deleted`),value:String((C?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,O.jsx)(j,{label:t(`messages.outgoing`),value:String((C?.rows??[]).filter(e=>e.Outgoing).length)}),(0,O.jsx)(j,{label:t(`messages.ownerPeer`),value:n&&i?`${Ge(n)} / ${Ge(i)}`:`-`})]}),(0,O.jsxs)(`div`,{className:`operation-row`,children:[(0,O.jsxs)(`div`,{className:`operation-box`,children:[(0,O.jsxs)(`div`,{className:`operation-title`,children:[(0,O.jsx)(we,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,O.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,O.jsxs)(`label`,{className:`checkline`,children:[(0,O.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,O.jsx)(N,{path:`/api/actions/delete-messages`,label:t(`messages.previewDelete`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,ids:Xe(f,t(`messages.msgIDsInvalid`)),revoke:m})})]}),(0,O.jsxs)(`div`,{className:`operation-box`,children:[(0,O.jsxs)(`div`,{className:`operation-title`,children:[(0,O.jsx)(pe,{size:15}),` `,t(`messages.clearHistory`)]}),(0,O.jsx)(`input`,{value:y,onChange:e=>ee(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,O.jsx)(`input`,{value:S,onChange:e=>te(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,O.jsxs)(`label`,{className:`checkline`,children:[(0,O.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,O.jsxs)(`label`,{className:`checkline`,children:[(0,O.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,O.jsx)(N,{path:`/api/actions/delete-history`,label:t(`messages.previewClearHistory`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,max_id:Ye(y),max_batches:Ye(S),just_clear:_,revoke:m})})]})]}),(0,O.jsx)(`div`,{className:`table-wrap`,children:(0,O.jsxs)(`table`,{className:`data-table`,children:[(0,O.jsx)(`thead`,{children:(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`th`,{children:t(`common.messageId`)}),(0,O.jsx)(`th`,{children:t(`common.time`)}),(0,O.jsx)(`th`,{children:t(`common.sender`)}),(0,O.jsx)(`th`,{children:t(`messages.direction`)}),(0,O.jsx)(`th`,{children:`PTS`}),(0,O.jsx)(`th`,{children:t(`common.status`)}),(0,O.jsx)(`th`,{children:t(`messages.body`)}),(0,O.jsx)(`th`,{})]})}),(0,O.jsxs)(`tbody`,{children:[C?.rows.map(n=>(0,O.jsxs)(`tr`,{children:[(0,O.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,O.jsx)(`td`,{children:Je(n.Date)}),(0,O.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,O.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,O.jsx)(`td`,{children:n.PTS}),(0,O.jsx)(`td`,{children:n.Deleted?(0,O.jsx)(A,{tone:`danger`,children:t(`common.deleted`)}):(0,O.jsx)(A,{children:t(`common.survived`)})}),(0,O.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,O.jsx)(`td`,{children:(0,O.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,O.jsx)(ue,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!C||C.rows.length===0)&&(0,O.jsx)(it,{colSpan:8})]})]})})]})}function wt({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1];return n?(0,O.jsx)(ut,{id:Number(n),navigate:t}):r?(0,O.jsx)(ht,{id:Number(r),navigate:t}):e.path===`/accounts`?(0,O.jsx)(mt,{navigate:t}):e.path===`/channels`?(0,O.jsx)(gt,{navigate:t}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,O.jsx)(St,{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,O.jsx)(yt,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups`?(0,O.jsx)(P,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,O.jsx)(Ct,{navigate:t}):(0,O.jsx)(_t,{navigate:t})}function Tt(){let[e,t]=(0,g.useState)(void 0),[n,r]=(0,g.useState)(()=>Ie());(0,g.useEffect)(()=>{let e=()=>r(Ie());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(Ie())};return e===void 0?(0,O.jsx)(Be,{}):e===null?(0,O.jsx)(st,{onLogin:t}):(0,O.jsx)(Ve,{actor:e,route:n,navigate:i,onLogout:()=>t(null),children:(0,O.jsx)(wt,{route:n,navigate:i})})}_.createRoot(document.getElementById(`root`)).render((0,O.jsx)(g.StrictMode,{children:(0,O.jsx)(je,{children:(0,O.jsx)(Tt,{})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/assets/index-Q8RNNOYL.js b/cmd/telesrv-admin/web/dist/assets/index-Q8RNNOYL.js new file mode 100644 index 00000000..bcab617c --- /dev/null +++ b/cmd/telesrv-admin/web/dist/assets/index-Q8RNNOYL.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{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=U.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Te(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Ee={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},De=[`Webkit`,`ms`,`Moz`,`O`];Object.keys(Ee).forEach(function(e){De.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Ee[t]=Ee[e]})});function Oe(e,t,n){return t==null||typeof t==`boolean`||t===``?``:n||typeof t!=`number`||t===0||Ee.hasOwnProperty(e)&&Ee[e]?(``+t).trim():t+`px`}function ke(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=n.indexOf(`--`)===0,i=Oe(n,t[n],r);n===`float`&&(n=`cssFloat`),r?e.setProperty(n,i):e[n]=i}}var Ae=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 je(e,t){if(t){if(Ae[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 Me(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 Ne=null;function Pe(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Fe=null,Ie=null,Le=null;function Re(e){if(e=ji(e)){if(typeof Fe!=`function`)throw Error(r(280));var t=e.stateNode;t&&(t=Ni(t),Fe(e.stateNode,e.type,t))}}function ze(e){Ie?Le?Le.push(e):Le=[e]:Ie=e}function Be(){if(Ie){var e=Ie,t=Le;if(Le=Ie=null,Re(e),t)for(e=0;e>>=0,e===0?32:31-(vt(e)/yt|0)|0}var xt=64,St=4194304;function Ct(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 wt(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=Ct(a))):r=Ct(s)}else o=n&~i,o===0?a!==0&&(r=Ct(a)):r=Ct(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 At(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-_t(t),e[t]=n}function jt(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=X),Jn=` `,Yn=!1;function Xn(e,t){switch(e){case`keyup`:return Wn.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`||!Gn&&Xn(e,t)?(e=hn(),mn=pn=fn=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=Y;try{var n=Xi;for(Y=1;e>=o,i-=o,la=1<<32-_t(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,Mt(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{Y=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,Mt(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-_t(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=Me(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*st()-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=st(),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=an,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},an=!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(ht&&typeof ht.onCommitFiberUnmount==`function`)try{ht.onCommitFiberUnmount(mt,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),nn(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=st()-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;lst()-$c?Tl(e,0):Xc|=n),hl(e,t)}function Bl(e,t){t===0&&(e.mode&1?(t=St,St<<=1,!(St&130023424)&&(St=4194304)):t=1);var n=fl();e=qa(e,t),e!==null&&(At(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 rt(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=kt(0),this.expirationTimes=kt(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=kt(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}`),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()}`),gifts:()=>y(`/api/gifts`),giftAnimation:e=>y(`/api/gifts/${e}/animation`),giftCollectibles:e=>y(`/api/gifts/${e}/collectibles`),giftCollectibleAnimation:(e,t,n)=>y(`/api/gifts/${e}/collectibles/${t}/${n}/animation`),importGift:e=>y(`/api/actions/import-gift`,{method:`POST`,body:e}),publishGiftCollectibles:(e,t)=>y(`/api/actions/publish-gift-collectibles?gift_id=${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(`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`}]]),M=E(`ArrowLeft`,[[`path`,{d:`m12 19-7-7 7-7`,key:`1l729n`}],[`path`,{d:`M19 12H5`,key:`x3x0zl`}]]),N=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`}]]),P=E(`Check`,[[`path`,{d:`M20 6 9 17l-5-5`,key:`1gmf2c`}]]),F=E(`ChevronDown`,[[`path`,{d:`m6 9 6 6 6-6`,key:`qrunsl`}]]),I=E(`ChevronRight`,[[`path`,{d:`m9 18 6-6-6-6`,key:`mthhwq`}]]),L=E(`Clock3`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`polyline`,{points:`12 6 12 12 16.5 12`,key:`1aq6pp`}]]),ee=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`}]]),R=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`}]]),te=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`}]]),ne=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`}]]),re=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`}]]),ie=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`}]]),ae=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`}]]),oe=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`}]]),se=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`}]]),ce=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`}]]),le=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`}]]),ue=E(`Play`,[[`polygon`,{points:`6 3 20 12 6 21 6 3`,key:`1oa8hb`}]]),z=E(`Plus`,[[`path`,{d:`M5 12h14`,key:`1ays0h`}],[`path`,{d:`M12 5v14`,key:`s699le`}]]),de=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`}]]),B=E(`Search`,[[`circle`,{cx:`11`,cy:`11`,r:`8`,key:`4ej97u`}],[`path`,{d:`m21 21-4.3-4.3`,key:`1qie3q`}]]),fe=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`}]]),pe=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`}]]),me=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`}]]),he=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`}]]),ge=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`}]]),_e=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`}]]),ve=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`}]]),ye=E(`X`,[[`path`,{d:`M18 6 6 18`,key:`1bl5f8`}],[`path`,{d:`m6 6 12 12`,key:`d8bk6v`}]]),be=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})),V=o(((e,t)=>{t.exports=be()}))(),H=`telesrv.admin.lang`,xe={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.dashboard":`Operations Console`,"route.dashboardSubtitle":`Console / Overview`,"route.messages":`Message Audit`,"route.messagesSubtitle":`Console / Messages`,"route.gifts":`Star Gifts`,"route.giftsSubtitle":`Console / Star Gifts`,"layout.navigation":`Navigation`,"layout.primaryNav":`Primary navigation`,"layout.dashboard":`Overview`,"layout.accounts":`Accounts`,"layout.channels":`Supergroups / Channels`,"layout.messages":`Messages`,"layout.gifts":`Star 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":`中文`,"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`,"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`,"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.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 rarity total 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":`Every section must total exactly 1000‰.`,"collectibles.colorHint":`Colors are stored as Telegram 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`,"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.dashboard":`运维控制台`,"route.dashboardSubtitle":`控制台 / 总览`,"route.messages":`消息审计`,"route.messagesSubtitle":`控制台 / 消息`,"route.gifts":`星星礼物`,"route.giftsSubtitle":`控制台 / 星星礼物`,"layout.navigation":`导航`,"layout.primaryNav":`主导航`,"layout.dashboard":`总览`,"layout.accounts":`账号`,"layout.channels":`超级群/频道`,"layout.messages":`消息`,"layout.gifts":`礼物目录`,"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":`中文`,"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":`频道/群`,"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":`星星礼物目录`,"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.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":`每一类的稀有度总和必须正好为 1000‰。`,"collectibles.colorHint":`颜色会按 Telegram 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":`发布版本`,"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":`时间`}},Se=(0,g.createContext)(null);function Ce({children:e}){let[t,n]=(0,g.useState)(()=>Ee());(0,g.useEffect)(()=>{try{localStorage.setItem(H,t)}catch{}document.documentElement.lang=t===`zh`?`zh-CN`:`en`,document.documentElement.dir=`ltr`,document.documentElement.setAttribute(`translate`,`no`),document.body.classList.add(`notranslate`),document.title=Te(t,`app.title`)},[t]);let r=(0,g.useMemo)(()=>({lang:t,setLang:n,t:(e,n)=>Te(t,e,n)}),[t]);return(0,V.jsx)(Se.Provider,{value:r,children:e})}function U(){let e=(0,g.useContext)(Se);if(!e)throw Error(`useI18n must be used inside I18nProvider`);return e}function we(){let{lang:e,setLang:t,t:n}=U();return(0,V.jsx)(`div`,{className:`language-switch`,role:`group`,"aria-label":`Language`,children:[`en`,`zh`].map(r=>(0,V.jsx)(`button`,{className:e===r?`active`:``,type:`button`,"aria-pressed":e===r,onClick:()=>t(r),children:n(`language.${r}`)},r))})}function Te(e,t,n){let r=xe[e][t]??xe.en[t]??t;return n?r.replace(/\{(\w+)\}/g,(e,t)=>String(n[t]??``)):r}function Ee(){try{let e=De(new URLSearchParams(window.location.search).get(`lang`));if(e)return e}catch{}try{let e=De(localStorage.getItem(H));if(e)return e}catch{}let e=navigator.languages?.length?navigator.languages:[navigator.language];for(let t of e){let e=De(t);if(e)return e}return`en`}function De(e){if(!e)return null;let t=e.trim().toLowerCase().replace(`_`,`-`);return t===`zh`||t.startsWith(`zh-`)?`zh`:t===`en`||t.startsWith(`en-`)?`en`:null}function Oe(){return{href:`${window.location.pathname}${window.location.search}`,path:window.location.pathname,search:new URLSearchParams(window.location.search)}}function ke(e,t){return e.startsWith(`/accounts`)?t(`route.accounts`):e.startsWith(`/channels`)?t(`route.channels`):e.startsWith(`/messages`)?t(`route.messages`):e.startsWith(`/gifts`)?t(`route.gifts`):t(`route.dashboard`)}function Ae(e,t){return e.startsWith(`/accounts`)?t(`route.accountsSubtitle`):e.startsWith(`/channels`)?t(`route.channelsSubtitle`):e.startsWith(`/messages`)?t(`route.messagesSubtitle`):e.startsWith(`/gifts`)?t(`route.giftsSubtitle`):t(`route.dashboardSubtitle`)}function je({href:e,navigate:t,className:n,children:r}){return(0,V.jsx)(`a`,{className:n,href:e,onClick:n=>{n.preventDefault(),t(e)},children:r})}function Me(){let{t:e}=U();return(0,V.jsxs)(`div`,{className:`boot-screen`,children:[(0,V.jsxs)(`div`,{className:`brand compact brand-elevated`,children:[(0,V.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,V.jsxs)(`span`,{children:[(0,V.jsx)(`strong`,{children:`telesrv`}),(0,V.jsx)(`small`,{children:e(`app.adminConsole`)})]})]}),(0,V.jsx)(`div`,{className:`loader-bar`})]})}function Ne({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,V.jsxs)(`div`,{className:`shell`,children:[(0,V.jsxs)(`aside`,{className:`sidebar`,children:[(0,V.jsxs)(je,{className:`brand`,href:`/`,navigate:n,children:[(0,V.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,V.jsxs)(`span`,{children:[(0,V.jsx)(`strong`,{children:`telesrv`}),(0,V.jsx)(`small`,{children:a(`app.adminConsole`)})]})]}),(0,V.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.navigation`)}),(0,V.jsxs)(`nav`,{className:`nav-list`,"aria-label":a(`layout.primaryNav`),children:[(0,V.jsx)(Pe,{icon:(0,V.jsx)(oe,{size:16}),href:`/`,route:t,navigate:n,children:a(`layout.dashboard`)}),(0,V.jsx)(Pe,{icon:(0,V.jsx)(ve,{size:16}),href:`/accounts`,route:t,navigate:n,children:a(`layout.accounts`)}),(0,V.jsx)(Pe,{icon:(0,V.jsx)(pe,{size:16}),href:`/channels`,route:t,navigate:n,children:a(`layout.channels`)}),(0,V.jsx)(Pe,{icon:(0,V.jsx)(re,{size:16}),href:`/gifts`,route:t,navigate:n,children:a(`layout.gifts`)}),(0,V.jsxs)(`div`,{className:`nav-section ${o?`active`:``} ${s?`open`:``}`,children:[(0,V.jsxs)(`button`,{className:`nav-section-toggle`,type:`button`,"aria-expanded":s,onClick:()=>c(e=>!e),children:[(0,V.jsx)(ce,{size:16}),(0,V.jsx)(`span`,{children:a(`layout.messages`)}),(0,V.jsx)(F,{className:`nav-section-chevron`,size:15})]}),s&&(0,V.jsxs)(`div`,{className:`nav-children`,children:[(0,V.jsx)(Pe,{href:`/messages/private`,route:t,navigate:n,activeWhen:e=>e===`/messages`||e===`/messages/detail`||e.startsWith(`/messages/private`),children:a(`layout.privateMessages`)}),(0,V.jsx)(Pe,{href:`/messages/groups`,route:t,navigate:n,activeWhen:e=>e.startsWith(`/messages/groups`),children:a(`layout.groupMessages`)})]})]})]}),(0,V.jsxs)(`div`,{className:`sidebar-status`,children:[(0,V.jsx)(`div`,{className:`sidebar-label`,children:a(`layout.runtime`)}),(0,V.jsxs)(`div`,{className:`runtime-row`,children:[(0,V.jsx)(fe,{size:14}),(0,V.jsx)(`span`,{children:a(`layout.adminBackend`)}),(0,V.jsx)(`strong`,{children:a(`layout.ready`)})]}),(0,V.jsxs)(`div`,{className:`runtime-row`,children:[(0,V.jsx)(ee,{size:14}),(0,V.jsx)(`span`,{children:a(`layout.pgRead`)}),(0,V.jsx)(`strong`,{children:a(`layout.readOnly`)})]}),(0,V.jsxs)(`div`,{className:`runtime-row`,children:[(0,V.jsx)(me,{size:14}),(0,V.jsx)(`span`,{children:a(`layout.writeOps`)}),(0,V.jsx)(`strong`,{children:a(`layout.dryRun`)})]})]})]}),(0,V.jsxs)(`div`,{className:`workspace`,children:[(0,V.jsxs)(`header`,{className:`topbar`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`div`,{className:`eyebrow`,children:Ae(t.path,a)}),(0,V.jsx)(`h1`,{children:ke(t.path,a)})]}),(0,V.jsxs)(`div`,{className:`topbar-actions`,children:[(0,V.jsx)(we,{}),(0,V.jsx)(`span`,{className:`actor-pill`,children:a(`layout.actor`,{actor:e})}),(0,V.jsxs)(`button`,{className:`btn ghost icon-text`,type:`button`,onClick:l,title:a(`layout.logout`),children:[(0,V.jsx)(se,{size:16}),` `,a(`layout.logout`)]})]})]}),(0,V.jsx)(`main`,{className:`content`,children:i})]})]})}function Pe({href:e,route:t,navigate:n,icon:r,children:i,activeWhen:a}){return(0,V.jsxs)(je,{className:`nav-item ${(a?a(t.path):e===`/`?t.path===`/`:t.path.startsWith(e))?`active`:``}`,href:e,navigate:n,children:[r??(0,V.jsx)(`span`,{"aria-hidden":`true`,className:`nav-dot`}),(0,V.jsx)(`span`,{children:i})]})}function Fe(e){let t=e.trim();return!t||t.startsWith(`+`)?t:/^\d+$/.test(t)?`+${t}`:t}function Ie(e){let t=e.trim();return t?t.startsWith(`@`)?t:`@${t}`:``}function Le(e){return`${e.FirstName||``} ${e.LastName||``}`.trim()||`-`}function Re(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 ze(e){if(!e||e.startsWith(`0001-`))return``;let t=new Date(e);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function Be(e){if(!e||e<=0)return``;let t=new Date(e*1e3);return Number.isNaN(t.getTime())?``:t.toLocaleString()}function W(e){if(!e.trim())return 0;let t=Number.parseInt(e,10);return Number.isFinite(t)?t:0}function Ve(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 He({title:e,eyebrow:t,children:n,actions:r}){return(0,V.jsxs)(`div`,{className:`page-frame`,children:[(0,V.jsxs)(`div`,{className:`page-title-row`,children:[(0,V.jsxs)(`div`,{children:[t&&(0,V.jsx)(`div`,{className:`eyebrow`,children:t}),(0,V.jsx)(`h2`,{children:e})]}),r&&(0,V.jsx)(`div`,{className:`page-actions`,children:r})]}),n]})}function Ue({children:e}){return(0,V.jsx)(`div`,{className:`query-panel`,children:e})}function We({main:e,side:t}){return(0,V.jsxs)(`div`,{className:`split-layout`,children:[(0,V.jsx)(`div`,{className:`split-main`,children:e}),(0,V.jsx)(`aside`,{className:`split-side`,children:t})]})}function Ge({title:e,text:t,action:n}){return(0,V.jsxs)(`div`,{className:`section-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`h2`,{children:e}),t&&(0,V.jsx)(`p`,{children:t})]}),n&&(0,V.jsx)(`div`,{className:`section-action`,children:n})]})}function Ke({children:e}){return(0,V.jsxs)(`div`,{className:`alert`,children:[(0,V.jsx)(O,{size:16}),` `,(0,V.jsx)(`span`,{children:e})]})}function G({children:e,tone:t=`neutral`}){return(0,V.jsx)(`span`,{className:`badge ${t}`,children:e})}function K({label:e,value:t,tone:n}){return(0,V.jsxs)(`div`,{className:`status-item ${n}`,children:[(0,V.jsx)(`span`,{children:e}),(0,V.jsx)(`strong`,{children:t})]})}function q({label:e,value:t,tone:n=`neutral`,mono:r=!1}){return(0,V.jsxs)(`div`,{className:`metric ${n}`,children:[(0,V.jsx)(`span`,{children:e}),(0,V.jsx)(`strong`,{className:r?`mono`:``,children:t})]})}function J({label:e,value:t,mono:n=!1}){return(0,V.jsxs)(`div`,{className:`summary-item`,children:[(0,V.jsx)(`span`,{children:e}),(0,V.jsx)(`strong`,{className:n?`mono`:``,children:t})]})}function qe({rows:e}){let{t}=U();return(0,V.jsx)(`div`,{className:`table-wrap`,children:(0,V.jsxs)(`table`,{className:`data-table`,children:[(0,V.jsx)(`thead`,{children:(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`th`,{children:t(`audit.id`)}),(0,V.jsx)(`th`,{children:t(`audit.commandID`)}),(0,V.jsx)(`th`,{children:t(`audit.action`)}),(0,V.jsx)(`th`,{children:t(`audit.actor`)}),(0,V.jsx)(`th`,{children:t(`audit.status`)}),(0,V.jsx)(`th`,{children:t(`audit.dryRun`)}),(0,V.jsx)(`th`,{children:t(`audit.reason`)}),(0,V.jsx)(`th`,{children:t(`audit.time`)})]})}),(0,V.jsxs)(`tbody`,{children:[e.map(e=>(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`td`,{children:e.ID}),(0,V.jsx)(`td`,{className:`mono`,children:e.CommandID}),(0,V.jsx)(`td`,{children:e.Action}),(0,V.jsx)(`td`,{children:e.Actor}),(0,V.jsx)(`td`,{children:e.Status}),(0,V.jsx)(`td`,{children:e.DryRun?t(`common.yes`):t(`common.no`)}),(0,V.jsx)(`td`,{className:`truncate`,children:e.Reason}),(0,V.jsx)(`td`,{children:ze(e.CreatedAt)})]},e.ID)),e.length===0&&(0,V.jsx)(Je,{colSpan:8})]})]})})}function Je({colSpan:e}){let{t}=U();return(0,V.jsx)(`tr`,{children:(0,V.jsx)(`td`,{colSpan:e,className:`empty-cell`,children:t(`common.noResults`)})})}function Ye({label:e}){return(0,V.jsx)(`section`,{className:`surface`,children:(0,V.jsx)(`div`,{className:`loading-line`,children:e})})}function Xe({value:e}){return(0,V.jsx)(`pre`,{className:`json-block`,children:e||`{}`})}function Ze({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,V.jsx)(`main`,{className:`login-page`,children:(0,V.jsxs)(`section`,{className:`login-panel`,children:[(0,V.jsxs)(`div`,{className:`login-head`,children:[(0,V.jsxs)(`div`,{className:`brand brand-elevated`,children:[(0,V.jsx)(`span`,{className:`brand-mark`,children:`T`}),(0,V.jsxs)(`span`,{children:[(0,V.jsx)(`strong`,{children:`telesrv`}),(0,V.jsx)(`small`,{children:t(`app.adminConsole`)})]})]}),(0,V.jsxs)(`div`,{className:`login-head-actions`,children:[(0,V.jsx)(we,{}),(0,V.jsx)(`span`,{className:`login-chip`,children:t(`app.localAccess`)})]})]}),(0,V.jsxs)(`div`,{className:`login-copy`,children:[(0,V.jsx)(`h1`,{children:t(`login.heading`)}),(0,V.jsx)(`p`,{children:t(`login.body`)})]}),i&&(0,V.jsx)(Ke,{children:i}),(0,V.jsxs)(`form`,{className:`form-stack`,onSubmit:c,children:[(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:t(`login.secret`)}),(0,V.jsx)(`input`,{autoFocus:!0,type:`password`,value:n,autoComplete:`current-password`,onChange:e=>r(e.target.value)})]}),(0,V.jsx)(`button`,{className:`btn primary full`,type:`submit`,disabled:o,children:t(o?`login.submitting`:`login.submit`)})]})]})})}var Qe=m();function $e({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,V.jsxs)(V.Fragment,{children:[(0,V.jsxs)(`button`,{className:w,type:`button`,onClick:()=>{y(),l(!0)},children:[r,e]}),c&&(0,Qe.createPortal)((0,V.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,V.jsxs)(`section`,{className:`modal command-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,V.jsxs)(`div`,{className:`modal-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`div`,{className:`eyebrow`,children:s(`action.flow`)}),(0,V.jsx)(`h2`,{children:e})]}),(0,V.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>l(!1),"aria-label":s(`action.close`),children:(0,V.jsx)(ye,{size:15})})]}),(0,V.jsxs)(`div`,{className:`command-body`,children:[(0,V.jsxs)(`div`,{className:`command-steps`,children:[(0,V.jsxs)(`div`,{className:`command-step ${u.trim()?`done`:`active`}`,children:[(0,V.jsx)(`span`,{children:`1`}),(0,V.jsx)(`strong`,{children:s(`action.stepReason`)})]}),(0,V.jsxs)(`div`,{className:`command-step ${f?.dry_run?`done`:u.trim()?`active`:``}`,children:[(0,V.jsx)(`span`,{children:`2`}),(0,V.jsx)(`strong`,{children:s(`action.stepDryRun`)})]}),(0,V.jsxs)(`div`,{className:`command-step ${f&&!f.dry_run&&!f.error?`done`:C?`active`:``}`,children:[(0,V.jsx)(`span`,{children:`3`}),(0,V.jsx)(`strong`,{children:s(`action.stepConfirm`)})]})]}),(0,V.jsxs)(`label`,{className:`form-field`,children:[(0,V.jsx)(`span`,{children:s(`action.reason`)}),(0,V.jsx)(`textarea`,{value:u,onChange:e=>d(e.target.value),rows:3,placeholder:s(`action.reasonPlaceholder`)})]}),(0,V.jsxs)(`div`,{className:`command-preview`,children:[(0,V.jsxs)(`div`,{className:`preview-head`,children:[(0,V.jsx)(te,{size:14}),` `,s(`action.requestPreview`)]}),(0,V.jsx)(Xe,{value:JSON.stringify(T,null,2)})]}),m&&(0,V.jsx)(Ke,{children:m}),f&&(0,V.jsxs)(`div`,{className:`result-box`,children:[(0,V.jsxs)(`div`,{className:`result-title`,children:[f.error?(0,V.jsx)(O,{size:16}):(0,V.jsx)(k,{size:16}),(0,V.jsx)(`strong`,{children:f.message||f.error||s(`action.result`)})]}),(0,V.jsxs)(`div`,{className:`result-line`,children:[(0,V.jsx)(`span`,{children:s(`action.commandID`)}),(0,V.jsx)(`strong`,{children:f.command_id})]}),(0,V.jsxs)(`div`,{className:`result-line`,children:[(0,V.jsx)(`span`,{children:s(`action.status`)}),(0,V.jsx)(`strong`,{children:f.status})]}),(0,V.jsxs)(`div`,{className:`result-line`,children:[(0,V.jsx)(`span`,{children:s(`action.dryRun`)}),(0,V.jsx)(`strong`,{children:f.dry_run?s(`common.yes`):s(`common.no`)})]}),(0,V.jsx)(`div`,{className:`result-message`,children:f.message||f.error}),f.details&&(0,V.jsx)(Xe,{value:JSON.stringify(f.details,null,2)})]})]}),(0,V.jsxs)(`div`,{className:`modal-actions`,children:[(0,V.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>l(!1),children:s(`common.close`)}),(0,V.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>S(!1),disabled:_,children:[_?(0,V.jsx)(A,{size:15,className:`spin`}):(0,V.jsx)(ue,{size:15}),s(f?`action.runAgain`:`action.runDry`)]}),(0,V.jsxs)(`button`,{className:`btn danger icon-text`,type:`button`,onClick:()=>S(!0),disabled:_||!C,children:[(0,V.jsx)(k,{size:15}),s(`action.confirm`)]})]})]})}),document.body)]})}function et({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,V.jsxs)(`div`,{className:`authorization-block`,children:[(0,V.jsx)(`div`,{className:`table-wrap`,children:(0,V.jsxs)(`table`,{className:`data-table authorization-table`,children:[(0,V.jsx)(`thead`,{children:(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`th`,{children:r(`auth.device`)}),(0,V.jsx)(`th`,{children:r(`auth.platform`)}),(0,V.jsx)(`th`,{children:r(`auth.ip`)}),(0,V.jsx)(`th`,{children:r(`auth.lastActive`)}),(0,V.jsx)(`th`,{className:`device-actions-head`,children:r(`common.actions`)})]})}),(0,V.jsxs)(`tbody`,{children:[o.map(n=>(0,V.jsxs)(`tr`,{children:[(0,V.jsxs)(`td`,{className:`device-text`,children:[n.DeviceModel,` `,n.SystemVersion]}),(0,V.jsxs)(`td`,{className:`device-text`,children:[n.Platform,` `,n.AppVersion]}),(0,V.jsx)(`td`,{children:n.IP}),(0,V.jsx)(`td`,{children:ze(n.ActiveAt)}),(0,V.jsx)(`td`,{className:`device-actions-cell`,children:(0,V.jsxs)(`div`,{className:`device-actions`,children:[(0,V.jsx)($e,{label:r(`auth.revokeCurrent`),icon:(0,V.jsx)(se,{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,V.jsx)($e,{label:r(`auth.keepCurrent`),icon:(0,V.jsx)(pe,{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,V.jsx)(Je,{colSpan:5})]})]})}),(0,V.jsx)(`div`,{className:`danger-zone`,children:(0,V.jsx)($e,{label:r(`auth.revokeAll`),icon:(0,V.jsx)(N,{size:15}),path:`/api/actions/revoke-sessions`,payload:()=>({user_id:t,revoke_all:!0}),onDone:()=>s(()=>new Set(e.map(e=>e.Hash)))})})]})}function tt({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)(()=>nt(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(nt(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,V.jsx)(Ke,{children:a});if(!r)return(0,V.jsx)(Ye,{label:n(s?`account.loadingDetail`:`account.waitingData`)});let y=r.Account;return(0,V.jsx)(He,{title:n(`account.detailTitle`,{id:y.ID}),eyebrow:n(`account.profile`),actions:(0,V.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/accounts`),children:[(0,V.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,V.jsx)(We,{main:(0,V.jsxs)(`div`,{className:`stacked-sections`,children:[(0,V.jsxs)(`section`,{className:`entity-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`div`,{className:`entity-title`,children:Le(y)}),(0,V.jsxs)(`div`,{className:`entity-subtitle`,children:[Ie(y.Username)||n(`account.noUsername`),` · `,Fe(y.Phone)||n(`account.noPhone`)]})]}),(0,V.jsxs)(`div`,{className:`entity-badges`,children:[y.PremiumUntil>0?(0,V.jsx)(G,{tone:`good`,children:n(`account.premium`)}):(0,V.jsx)(G,{children:n(`account.notPremium`)}),r.Verified?(0,V.jsx)(G,{tone:`good`,children:n(`common.verified`)}):(0,V.jsx)(G,{children:n(`account.notVerified`)}),y.Frozen?(0,V.jsx)(G,{tone:`danger`,children:n(`account.accountFrozen`)}):(0,V.jsx)(G,{children:n(`account.accountActive`)})]})]}),(0,V.jsxs)(`div`,{className:`summary-grid`,children:[(0,V.jsx)(J,{label:n(`account.userID`),value:String(y.ID),mono:!0}),(0,V.jsx)(J,{label:n(`account.lastActive`),value:Be(r.LastSeenAt)||`-`}),(0,V.jsx)(J,{label:n(`account.premiumUntil`),value:y.PremiumUntil>0?Be(y.PremiumUntil):n(`common.none`)}),(0,V.jsx)(J,{label:n(`account.starsBalance`),value:`${r.StarsBalance} / ${r.StarsGranted?n(`account.startingGrantApplied`):n(`account.startingGrantPending`)}`}),(0,V.jsx)(J,{label:n(`common.updatedAt`),value:ze(y.UpdatedAt)||`-`}),(0,V.jsx)(J,{label:n(`account.activeSessions`),value:String(r.Authorizations.length)}),(0,V.jsx)(J,{label:n(`account.accountFlags`),value:`support=${r.Support} bot=${r.Bot}`}),(0,V.jsx)(J,{label:n(`account.restriction`),value:r.HasRestriction?r.Restriction.Reason||n(`account.restricted`):n(`common.none`)}),(0,V.jsx)(J,{label:n(`account.freezeSince`),value:r.Restriction.Since?ze(r.Restriction.Since):n(`common.none`)}),(0,V.jsx)(J,{label:n(`account.freezeUntil`),value:r.Restriction.Until?ze(r.Restriction.Until):n(`common.none`)}),(0,V.jsx)(J,{label:n(`account.freezeAppealURL`),value:r.Restriction.AppealURL||n(`common.none`)}),(0,V.jsx)(J,{label:n(`account.createdAt`),value:ze(y.CreatedAt)||`-`})]}),r.About&&(0,V.jsx)(`p`,{className:`about-text`,children:r.About}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:n(`account.authorizationsTitle`),text:n(`account.authorizationsCount`,{count:r.Authorizations.length})}),(0,V.jsx)(et,{rows:r.Authorizations,userID:y.ID,onDone:v})]}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,V.jsx)(qe,{rows:r.AuditLogs})]})]}),side:(0,V.jsxs)(`section`,{className:`action-dock`,children:[(0,V.jsx)(`div`,{className:`dock-title`,children:n(`account.actionDock`)}),(0,V.jsxs)(`label`,{className:`duration-field`,children:[(0,V.jsx)(`span`,{children:n(`account.freezeUntil`)}),(0,V.jsx)(`input`,{"aria-label":n(`account.freezeUntilAria`),value:p,onChange:e=>m(e.target.value),type:`datetime-local`})]}),(0,V.jsxs)(`label`,{className:`duration-field`,children:[(0,V.jsx)(`span`,{children:n(`account.freezeAppealURL`)}),(0,V.jsx)(`input`,{"aria-label":n(`account.freezeAppealURLAria`),value:h,onChange:e=>_(e.target.value),type:`url`,placeholder:`https://...`})]}),(0,V.jsx)($e,{label:y.Frozen?n(`account.updateFreeze`):n(`account.freezeAccount`),icon:(0,V.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,V.jsx)($e,{label:n(`account.unfreezeAccount`),icon:(0,V.jsx)(O,{size:15}),path:`/api/actions/set-frozen`,payload:()=>({user_id:y.ID,frozen:!1}),onDone:v}),(0,V.jsxs)(`label`,{className:`duration-field`,children:[(0,V.jsx)(`span`,{children:n(`account.premiumMonths`)}),(0,V.jsx)(`input`,{"aria-label":n(`account.premiumMonthsAria`),value:l,onChange:e=>u(e.target.value),type:`number`,min:`1`,max:`120`})]}),(0,V.jsxs)(`div`,{className:`action-stack`,children:[(0,V.jsx)($e,{label:n(`account.setPremium`),icon:(0,V.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:W(l)}),onDone:v}),(0,V.jsx)($e,{label:n(`account.clearPremium`),icon:(0,V.jsx)(j,{size:15}),tone:`warn`,path:`/api/actions/grant-premium`,payload:()=>({user_id:y.ID,months:0}),onDone:v}),(0,V.jsxs)(`label`,{className:`duration-field`,children:[(0,V.jsx)(`span`,{children:n(`account.starsAmount`)}),(0,V.jsx)(`input`,{"aria-label":n(`account.starsAmountAria`),value:d,onChange:e=>f(e.target.value),type:`number`,min:`1`,max:`1000000000`})]}),(0,V.jsx)($e,{label:n(`account.grantStars`),icon:(0,V.jsx)(he,{size:15}),tone:`warn`,path:`/api/actions/grant-stars`,payload:()=>({user_id:y.ID,amount:W(d)}),onDone:v}),(0,V.jsx)($e,{label:r.Verified?n(`account.clearVerified`):n(`account.setVerified`),icon:(0,V.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-verified`,payload:()=>({user_id:y.ID,verified:!r.Verified}),onDone:v})]})]})})})}function nt(e){return new Date(e.getTime()-e.getTimezoneOffset()*6e4).toISOString().slice(0,16)}function rt(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 it(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 at({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=rt(o?.rows??[]);return(0,V.jsxs)(He,{title:t(`account.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`account.recentActive`),actions:(0,V.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,V.jsx)(de,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,V.jsx)(Ke,{children:f}),(0,V.jsxs)(`div`,{className:`metric-row`,children:[(0,V.jsx)(q,{label:t(`account.currentPage`),value:String(o?.rows.length??0)}),(0,V.jsx)(q,{label:t(`account.onlineDevices`),value:String(h.devices)}),(0,V.jsx)(q,{label:t(`account.premium`),value:String(h.premium),tone:`good`}),(0,V.jsx)(q,{label:t(`account.frozen`),value:String(h.frozen),tone:h.frozen>0?`danger`:`neutral`})]}),(0,V.jsx)(Ue,{children:(0,V.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,V.jsxs)(`label`,{className:`searchbox`,children:[(0,V.jsx)(B,{size:15}),(0,V.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`account.searchPlaceholder`)})]}),(0,V.jsxs)(`label`,{className:`field-inline`,children:[(0,V.jsx)(`span`,{children:t(`common.limit`)}),(0,V.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,V.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,V.jsx)(A,{size:15,className:`spin`}):(0,V.jsx)(B,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,V.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,V.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,V.jsx)(`div`,{className:`table-wrap`,children:(0,V.jsxs)(`table`,{className:`data-table`,children:[(0,V.jsx)(`thead`,{children:(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`th`,{children:t(`account.userID`)}),(0,V.jsx)(`th`,{children:t(`account.phone`)}),(0,V.jsx)(`th`,{children:t(`common.username`)}),(0,V.jsx)(`th`,{children:t(`common.name`)}),(0,V.jsx)(`th`,{children:t(`common.device`)}),(0,V.jsx)(`th`,{children:t(`account.lastActive`)}),(0,V.jsx)(`th`,{children:t(`account.premium`)}),(0,V.jsx)(`th`,{children:t(`common.verified`)}),(0,V.jsx)(`th`,{children:t(`account.frozen`)}),(0,V.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,V.jsx)(`th`,{})]})}),(0,V.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`td`,{className:`mono`,children:n.ID}),(0,V.jsx)(`td`,{children:Fe(n.Phone)}),(0,V.jsx)(`td`,{children:Ie(n.Username)}),(0,V.jsx)(`td`,{children:Le(n)}),(0,V.jsx)(`td`,{children:n.DeviceCount}),(0,V.jsx)(`td`,{children:ze(n.LastActiveAt)}),(0,V.jsx)(`td`,{children:n.PremiumUntil>0?(0,V.jsxs)(G,{tone:`good`,children:[t(`account.premium`),` `,Be(n.PremiumUntil)]}):(0,V.jsx)(G,{children:t(`common.none`)})}),(0,V.jsx)(`td`,{children:n.Verified?(0,V.jsx)(G,{tone:`good`,children:t(`common.verified`)}):(0,V.jsx)(G,{children:t(`account.notVerified`)})}),(0,V.jsx)(`td`,{children:n.Frozen?(0,V.jsx)(G,{tone:`danger`,children:t(`account.frozen`)}):(0,V.jsx)(G,{children:t(`common.normal`)})}),(0,V.jsx)(`td`,{children:ze(n.UpdatedAt)}),(0,V.jsx)(`td`,{children:(0,V.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/accounts/${n.ID}`),children:[t(`common.detail`),` `,(0,V.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,V.jsx)(Je,{colSpan:11})]})]})})]})}function ot({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,V.jsx)(Ke,{children:a});if(!r)return(0,V.jsx)(Ye,{label:n(`channel.loadingDetail`)});let c=r.Channel;return(0,V.jsx)(He,{title:`${Re(c,n)} #${c.ID}`,eyebrow:n(`channel.detailProfile`),actions:(0,V.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>t(`/channels`),children:[(0,V.jsx)(M,{size:15}),` `,n(`common.backToList`)]}),children:(0,V.jsx)(We,{main:(0,V.jsxs)(`div`,{className:`stacked-sections`,children:[(0,V.jsxs)(`section`,{className:`entity-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`div`,{className:`entity-title`,children:c.Title||`-`}),(0,V.jsxs)(`div`,{className:`entity-subtitle`,children:[Ie(c.Username)||n(`account.noUsername`),` · `,n(`channel.creator`,{id:c.CreatorUserID})]})]}),(0,V.jsxs)(`div`,{className:`entity-badges`,children:[(0,V.jsx)(G,{children:Re(c,n)}),c.Verified?(0,V.jsx)(G,{tone:`good`,children:n(`common.verified`)}):(0,V.jsx)(G,{children:n(`account.notVerified`)}),c.Deleted?(0,V.jsx)(G,{tone:`danger`,children:n(`common.deleted`)}):(0,V.jsx)(G,{children:n(`common.valid`)})]})]}),(0,V.jsxs)(`div`,{className:`summary-grid`,children:[(0,V.jsx)(J,{label:n(`channel.channelID`),value:String(c.ID),mono:!0}),(0,V.jsx)(J,{label:`access_hash`,value:String(c.AccessHash),mono:!0}),(0,V.jsx)(J,{label:n(`common.members`),value:`${c.ParticipantsCount} / ${n(`common.admins`)} ${c.AdminsCount}`}),(0,V.jsx)(J,{label:n(`channel.governance`),value:n(`channel.governanceValue`,{banned:c.BannedCount,kicked:c.KickedCount})}),(0,V.jsx)(J,{label:n(`channel.flags`),value:`broadcast=${c.Broadcast} megagroup=${c.Megagroup} forum=${c.Forum}`}),(0,V.jsx)(J,{label:`top / pinned / PTS`,value:`${c.TopMessageID} / ${c.PinnedMessageID} / ${c.PTS}`}),(0,V.jsx)(J,{label:n(`account.createdAt`),value:Be(c.Date)||`-`}),(0,V.jsx)(J,{label:n(`common.updatedAt`),value:ze(c.UpdatedAt)||`-`})]}),c.About&&(0,V.jsx)(`p`,{className:`about-text`,children:c.About}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:n(`account.recentAdminOps`),text:n(`account.recent30Audit`)}),(0,V.jsx)(qe,{rows:r.AuditLogs})]}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:n(`channel.rawRow`),text:n(`channel.rawRowText`)}),(0,V.jsx)(Xe,{value:r.ChannelJSON})]})]}),side:(0,V.jsxs)(`section`,{className:`action-dock`,children:[(0,V.jsx)(`div`,{className:`dock-title`,children:n(`channel.actionDock`)}),(0,V.jsx)($e,{label:c.Verified?n(`channel.clearVerified`):n(`channel.setVerified`),icon:(0,V.jsx)(D,{size:15}),tone:`warn`,path:`/api/actions/set-channel-verified`,payload:()=>({channel_id:c.ID,verified:!c.Verified}),onDone:s})]})})})}function st({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=it(o?.rows??[]);return(0,V.jsxs)(He,{title:t(`channel.pageTitle`),eyebrow:o?.listing===!1?t(`account.queryResults`):t(`channel.recentUpdated`),actions:(0,V.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>m(!1),disabled:u,children:[(0,V.jsx)(de,{size:15}),` `,t(`common.refresh`)]}),children:[f&&(0,V.jsx)(Ke,{children:f}),(0,V.jsxs)(`div`,{className:`metric-row`,children:[(0,V.jsx)(q,{label:t(`channel.currentPage`),value:String(o?.rows.length??0)}),(0,V.jsx)(q,{label:t(`channel.megagroups`),value:String(h.megagroups)}),(0,V.jsx)(q,{label:t(`channel.broadcasts`),value:String(h.broadcasts)}),(0,V.jsx)(q,{label:t(`channel.verifiedCount`),value:String(h.verified),tone:`good`})]}),(0,V.jsx)(Ue,{children:(0,V.jsxs)(`form`,{className:`toolbar`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,V.jsxs)(`label`,{className:`searchbox`,children:[(0,V.jsx)(B,{size:15}),(0,V.jsx)(`input`,{value:n,onChange:e=>r(e.target.value),placeholder:t(`channel.searchPlaceholder`)})]}),(0,V.jsxs)(`label`,{className:`field-inline`,children:[(0,V.jsx)(`span`,{children:t(`common.limit`)}),(0,V.jsx)(`input`,{className:`small-input`,value:i,onChange:e=>a(e.target.value),type:`number`,min:`1`,max:`100`})]}),(0,V.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,disabled:u,children:[u?(0,V.jsx)(A,{size:15,className:`spin`}):(0,V.jsx)(B,{size:15}),` `,t(`common.search`)]}),o?.listing&&o.has_more&&(0,V.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),disabled:u,children:[(0,V.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]})]})}),(0,V.jsx)(`div`,{className:`table-wrap`,children:(0,V.jsxs)(`table`,{className:`data-table`,children:[(0,V.jsx)(`thead`,{children:(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`th`,{children:t(`channel.channelID`)}),(0,V.jsx)(`th`,{children:t(`channel.kind`)}),(0,V.jsx)(`th`,{children:t(`common.username`)}),(0,V.jsx)(`th`,{children:t(`channel.title`)}),(0,V.jsx)(`th`,{children:t(`common.members`)}),(0,V.jsx)(`th`,{children:t(`common.admins`)}),(0,V.jsx)(`th`,{children:`PTS`}),(0,V.jsx)(`th`,{children:t(`common.verified`)}),(0,V.jsx)(`th`,{children:t(`common.updatedAt`)}),(0,V.jsx)(`th`,{})]})}),(0,V.jsxs)(`tbody`,{children:[o?.rows.map(n=>(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`td`,{className:`mono`,children:n.ID}),(0,V.jsx)(`td`,{children:Re(n,t)}),(0,V.jsx)(`td`,{children:Ie(n.Username)}),(0,V.jsx)(`td`,{children:n.Title}),(0,V.jsx)(`td`,{children:n.ParticipantsCount}),(0,V.jsx)(`td`,{children:n.AdminsCount}),(0,V.jsx)(`td`,{children:n.PTS}),(0,V.jsx)(`td`,{children:n.Verified?(0,V.jsx)(G,{tone:`good`,children:t(`common.verified`)}):(0,V.jsx)(G,{children:t(`account.notVerified`)})}),(0,V.jsx)(`td`,{children:ze(n.UpdatedAt)}),(0,V.jsx)(`td`,{children:(0,V.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/channels/${n.ID}`),children:[t(`common.detail`),` `,(0,V.jsx)(I,{size:14})]})})]},n.ID)),(!o||o.rows.length===0)&&(0,V.jsx)(Je,{colSpan:10})]})]})})]})}function ct({navigate:e}){let{t}=U();return(0,V.jsxs)(`div`,{className:`dashboard-layout`,children:[(0,V.jsxs)(`section`,{className:`overview-band`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`div`,{className:`eyebrow`,children:t(`dashboard.eyebrow`)}),(0,V.jsx)(`h2`,{children:t(`dashboard.title`)})]}),(0,V.jsxs)(`div`,{className:`overview-metrics`,children:[(0,V.jsx)(K,{label:t(`dashboard.readPath`),value:t(`dashboard.readPathValue`),tone:`neutral`}),(0,V.jsx)(K,{label:t(`dashboard.writePath`),value:`Admin API`,tone:`good`}),(0,V.jsx)(K,{label:t(`dashboard.executionPolicy`),value:t(`dashboard.dryRunFirst`),tone:`warn`})]})]}),(0,V.jsxs)(`div`,{className:`command-grid`,children:[(0,V.jsx)(lt,{icon:(0,V.jsx)(ve,{}),title:t(`route.accounts`),text:t(`dashboard.accountsText`),href:`/accounts`,navigate:e}),(0,V.jsx)(lt,{icon:(0,V.jsx)(pe,{}),title:t(`route.channels`),text:t(`dashboard.channelsText`),href:`/channels`,navigate:e}),(0,V.jsx)(lt,{icon:(0,V.jsx)(ce,{}),title:t(`route.messages`),text:t(`dashboard.messagesText`),href:`/messages`,navigate:e})]}),(0,V.jsxs)(`section`,{className:`work-strip`,children:[(0,V.jsxs)(`div`,{className:`strip-item`,children:[(0,V.jsx)(k,{size:16}),(0,V.jsx)(`span`,{children:t(`dashboard.strip.dryRun`)})]}),(0,V.jsxs)(`div`,{className:`strip-item`,children:[(0,V.jsx)(ae,{size:16}),(0,V.jsx)(`span`,{children:t(`dashboard.strip.token`)})]}),(0,V.jsxs)(`div`,{className:`strip-item`,children:[(0,V.jsx)(L,{size:16}),(0,V.jsx)(`span`,{children:t(`dashboard.strip.pagination`)})]}),(0,V.jsxs)(`div`,{className:`strip-item`,children:[(0,V.jsx)(te,{size:16}),(0,V.jsx)(`span`,{children:t(`dashboard.strip.snapshot`)})]})]})]})}function lt({icon:e,title:t,text:n,href:r,navigate:i}){return(0,V.jsxs)(je,{className:`launcher`,href:r,navigate:i,children:[(0,V.jsx)(`span`,{className:`launcher-icon`,children:e}),(0,V.jsxs)(`span`,{className:`launcher-copy`,children:[(0,V.jsx)(`strong`,{children:t}),(0,V.jsx)(`span`,{children:n})]}),(0,V.jsx)(I,{size:16})]})}function ut({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,V.jsx)(Ke,{children:o});if(!i)return(0,V.jsx)(Ye,{label:r(`common.loading`)});let l=i.Message;return(0,V.jsx)(He,{title:r(`messages.groupDetailTitle`,{id:l.ID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,V.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/groups`),children:[(0,V.jsx)(M,{size:15}),` `,r(`messages.backGroup`)]}),children:(0,V.jsxs)(`div`,{className:`stacked-sections`,children:[(0,V.jsxs)(`section`,{className:`entity-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`div`,{className:`entity-title`,children:r(`messages.channelGroupTitle`,{id:l.ChannelID})}),(0,V.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.SenderUserID,date:Be(l.Date)})})]}),(0,V.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,V.jsx)(G,{tone:`danger`,children:r(`common.deleted`)}):(0,V.jsx)(G,{children:r(`common.survived`)}),l.Pinned&&(0,V.jsx)(G,{tone:`warn`,children:r(`messages.pinned`)}),l.Post&&(0,V.jsx)(G,{children:r(`messages.channelPost`)}),(0,V.jsxs)(G,{children:[`pts `,l.PTS]})]})]}),(0,V.jsxs)(`div`,{className:`summary-grid`,children:[(0,V.jsx)(J,{label:r(`common.messageId`),value:String(l.ID),mono:!0}),(0,V.jsx)(J,{label:r(`messages.channelGroup`),value:String(l.ChannelID),mono:!0}),(0,V.jsx)(J,{label:`From Peer`,value:`${l.FromPeerType}:${l.FromPeerID}`,mono:!0}),(0,V.jsx)(J,{label:r(`common.views`),value:String(l.ViewsCount)})]}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:r(`messages.channelMessageRow`),text:r(`messages.channelMessagesSnapshot`)}),(0,V.jsx)(Xe,{value:i.MessageJSON})]}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:r(`messages.channelRow`),text:r(`messages.channelSnapshot`)}),(0,V.jsx)(Xe,{value:i.ChannelJSON})]}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:r(`messages.channelUpdateEvents`),text:r(`messages.channelEventsSource`)}),(0,V.jsx)(`div`,{className:`table-wrap`,children:(0,V.jsxs)(`table`,{className:`data-table`,children:[(0,V.jsx)(`thead`,{children:(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`th`,{children:`PTS`}),(0,V.jsx)(`th`,{children:r(`common.count`)}),(0,V.jsx)(`th`,{children:r(`common.type`)}),(0,V.jsx)(`th`,{children:r(`common.messageId`)}),(0,V.jsx)(`th`,{children:r(`common.sender`)}),(0,V.jsx)(`th`,{children:r(`common.time`)})]})}),(0,V.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`td`,{children:e.PTS}),(0,V.jsx)(`td`,{children:e.PTSCount}),(0,V.jsx)(`td`,{children:e.Type}),(0,V.jsx)(`td`,{children:e.MessageID}),(0,V.jsx)(`td`,{children:e.SenderUserID}),(0,V.jsx)(`td`,{children:Be(e.Date)})]},`${e.PTS}-${e.Type}-${e.MessageID}`)),i.UpdateEvents.length===0&&(0,V.jsx)(Je,{colSpan:6})]})]})})]}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:r(`messages.eventJson`)}),(0,V.jsxs)(`div`,{className:`raw-grid`,children:[i.UpdateEvents.map(e=>(0,V.jsx)(Xe,{value:e.JSON},`${e.PTS}-${e.Type}-json`)),i.UpdateEvents.length===0&&(0,V.jsx)(`div`,{className:`empty-panel`,children:r(`common.noResults`)})]})]})]})})}function dt({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,V.jsxs)(`div`,{className:`entity-picker`,children:[(0,V.jsxs)(`div`,{className:`picker-head`,children:[(0,V.jsx)(`span`,{children:e}),t?(0,V.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,V.jsx)(ye,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,V.jsxs)(`div`,{className:`selected-entity`,children:[(0,V.jsx)(P,{size:15}),(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`strong`,{children:Le(t)}),(0,V.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,V.jsx)(`span`,{children:Ie(t.Username)||Fe(t.Phone)||`-`})]}):null,(0,V.jsxs)(`div`,{className:`picker-search`,children:[(0,V.jsx)(B,{size:15}),(0,V.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.userPlaceholder`)}),(0,V.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,V.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,V.jsx)(`div`,{className:`picker-error`,children:u}),(0,V.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,V.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,V.jsx)(`span`,{className:`mono`,children:e.ID}),(0,V.jsx)(`strong`,{children:Le(e)}),(0,V.jsx)(`span`,{children:Ie(e.Username)||Fe(e.Phone)||`-`}),e.Verified?(0,V.jsx)(G,{tone:`good`,children:r(`picker.verified`)}):(0,V.jsx)(G,{children:r(`picker.regular`)})]},e.ID)),o.length===0&&!c?(0,V.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function ft({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,V.jsxs)(`div`,{className:`entity-picker`,children:[(0,V.jsxs)(`div`,{className:`picker-head`,children:[(0,V.jsx)(`span`,{children:e}),t?(0,V.jsxs)(`button`,{className:`link-button`,type:`button`,onClick:()=>n(null),children:[(0,V.jsx)(ye,{size:13}),` `,r(`common.clear`)]}):null]}),t?(0,V.jsxs)(`div`,{className:`selected-entity`,children:[(0,V.jsx)(P,{size:15}),(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`strong`,{children:t.Title||`-`}),(0,V.jsx)(`span`,{className:`mono`,children:t.ID})]}),(0,V.jsx)(`span`,{children:Ie(t.Username)||Re(t,r)})]}):null,(0,V.jsxs)(`div`,{className:`picker-search`,children:[(0,V.jsx)(B,{size:15}),(0,V.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),onKeyDown:e=>{e.key===`Enter`&&(e.preventDefault(),f())},placeholder:r(`picker.channelPlaceholder`)}),(0,V.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:f,disabled:c,children:c?(0,V.jsx)(A,{size:14,className:`spin`}):r(`common.search`)})]}),u&&(0,V.jsx)(`div`,{className:`picker-error`,children:u}),(0,V.jsxs)(`div`,{className:`picker-results`,children:[o.map(e=>(0,V.jsxs)(`button`,{className:`picker-row ${t?.ID===e.ID?`selected`:``}`,type:`button`,onClick:()=>n(e),children:[(0,V.jsx)(`span`,{className:`mono`,children:e.ID}),(0,V.jsx)(`strong`,{children:e.Title||`-`}),(0,V.jsx)(`span`,{children:Ie(e.Username)||Re(e,r)}),e.Verified?(0,V.jsx)(G,{tone:`good`,children:r(`picker.verified`)}):(0,V.jsx)(G,{children:Re(e,r)})]},e.ID)),o.length===0&&!c?(0,V.jsx)(`div`,{className:`picker-empty`,children:r(`common.noResults`)}):null]})]})}function pt({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,V.jsxs)(He,{title:t(`messages.groupTitle`),eyebrow:t(`messages.groupEyebrow`),children:[f&&(0,V.jsx)(Ke,{children:f}),(0,V.jsxs)(Ue,{children:[(0,V.jsx)(`div`,{className:`message-selector-grid single`,children:(0,V.jsx)(ft,{label:t(`messages.channelGroup`),value:n,onChange:h})}),(0,V.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),m(!1)},children:[(0,V.jsx)(`input`,{value:i,onChange:e=>a(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,V.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,V.jsx)(`input`,{className:`small-input`,value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,V.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,V.jsx)(B,{size:15}),` `,t(`messages.searchMessages`)]}),_.length?(0,V.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>m(!0),children:[(0,V.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,V.jsxs)(`div`,{className:`metric-row`,children:[(0,V.jsx)(q,{label:t(`messages.currentPage`),value:String(_.length)}),(0,V.jsx)(q,{label:t(`messages.mediaCount`),value:String(_.filter(e=>e.Media&&e.Media!==`{}`).length)}),(0,V.jsx)(q,{label:t(`messages.channelPosts`),value:String(_.filter(e=>e.Post).length)}),(0,V.jsx)(q,{label:t(`messages.channelGroup`),value:n?`${n.Title||Re(n,t)} (${n.ID})`:`-`})]}),(0,V.jsx)(`div`,{className:`table-wrap`,children:(0,V.jsxs)(`table`,{className:`data-table`,children:[(0,V.jsx)(`thead`,{children:(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`th`,{children:t(`common.messageId`)}),(0,V.jsx)(`th`,{children:t(`common.time`)}),(0,V.jsx)(`th`,{children:t(`common.sender`)}),(0,V.jsx)(`th`,{children:`From Peer`}),(0,V.jsx)(`th`,{children:`PTS`}),(0,V.jsx)(`th`,{children:t(`common.views`)}),(0,V.jsx)(`th`,{children:t(`common.status`)}),(0,V.jsx)(`th`,{children:t(`messages.body`)}),(0,V.jsx)(`th`,{})]})}),(0,V.jsxs)(`tbody`,{children:[_.map(n=>(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`td`,{className:`mono`,children:n.ID}),(0,V.jsx)(`td`,{children:Be(n.Date)}),(0,V.jsx)(`td`,{className:`mono`,children:n.SenderUserID}),(0,V.jsxs)(`td`,{className:`mono`,children:[n.FromPeerType,`:`,n.FromPeerID]}),(0,V.jsx)(`td`,{children:n.PTS}),(0,V.jsx)(`td`,{children:n.ViewsCount}),(0,V.jsx)(`td`,{children:n.Deleted?(0,V.jsx)(G,{tone:`danger`,children:t(`common.deleted`)}):n.Pinned?(0,V.jsx)(G,{tone:`warn`,children:t(`messages.pinned`)}):(0,V.jsx)(G,{children:t(`common.survived`)})}),(0,V.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,V.jsx)(`td`,{children:(0,V.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/groups/detail?channel_id=${n.ChannelID}&msg_id=${n.ID}`),children:[t(`common.detail`),` `,(0,V.jsx)(I,{size:14})]})})]},`${n.ChannelID}-${n.ID}`)),_.length===0&&(0,V.jsx)(Je,{colSpan:9})]})]})})]})}function mt({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,V.jsx)(Ke,{children:o});if(!i)return(0,V.jsx)(Ye,{label:r(`common.loading`)});let l=i.Message;return(0,V.jsx)(He,{title:r(`messages.privateDetailTitle`,{id:l.BoxID}),eyebrow:r(`messages.detailEyebrow`),actions:(0,V.jsxs)(`button`,{className:`btn icon-text`,onClick:()=>n(`/messages/private`),children:[(0,V.jsx)(M,{size:15}),` `,r(`messages.backPrivate`)]}),children:(0,V.jsx)(We,{main:(0,V.jsxs)(`div`,{className:`stacked-sections`,children:[(0,V.jsxs)(`section`,{className:`entity-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`div`,{className:`entity-title`,children:r(`messages.ownerPeerTitle`,{owner:l.OwnerUserID,peer:l.PeerID})}),(0,V.jsx)(`div`,{className:`entity-subtitle`,children:r(`messages.senderSubtitle`,{sender:l.FromUserID,date:Be(l.Date)})})]}),(0,V.jsxs)(`div`,{className:`entity-badges`,children:[l.Deleted?(0,V.jsx)(G,{tone:`danger`,children:r(`common.deleted`)}):(0,V.jsx)(G,{children:r(`common.survived`)}),(0,V.jsxs)(G,{children:[`pts `,l.PTS]}),(0,V.jsx)(G,{children:l.Outgoing?r(`messages.outgoing`):r(`messages.incoming`)})]})]}),(0,V.jsxs)(`div`,{className:`summary-grid`,children:[(0,V.jsx)(J,{label:r(`messages.boxID`),value:String(l.BoxID),mono:!0}),(0,V.jsx)(J,{label:r(`messages.privateMessageID`),value:String(l.PrivateMessageID),mono:!0}),(0,V.jsx)(J,{label:r(`messages.messageSender`),value:String(l.MessageSenderID),mono:!0}),(0,V.jsx)(J,{label:r(`common.time`),value:Be(l.Date)})]}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:r(`messages.messageBox`),text:r(`messages.messageBoxesSnapshot`)}),(0,V.jsx)(Xe,{value:i.MessageJSON})]}),(0,V.jsxs)(`div`,{className:`raw-grid`,children:[(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:r(`messages.dialogRow`),text:r(`messages.dialogSnapshot`)}),(0,V.jsx)(Xe,{value:i.DialogJSON})]}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:r(`messages.privateRow`),text:r(`messages.privateSnapshot`)}),(0,V.jsx)(Xe,{value:i.PrivateJSON})]})]}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:r(`messages.userUpdateEvents`),text:r(`messages.userEventsSource`)}),(0,V.jsx)(`div`,{className:`table-wrap`,children:(0,V.jsxs)(`table`,{className:`data-table`,children:[(0,V.jsx)(`thead`,{children:(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`th`,{children:`PTS`}),(0,V.jsx)(`th`,{children:r(`common.count`)}),(0,V.jsx)(`th`,{children:r(`common.type`)}),(0,V.jsx)(`th`,{children:r(`common.time`)})]})}),(0,V.jsxs)(`tbody`,{children:[i.UpdateEvents.map(e=>(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`td`,{children:e.PTS}),(0,V.jsx)(`td`,{children:e.PTSCount}),(0,V.jsx)(`td`,{children:e.Type}),(0,V.jsx)(`td`,{children:Be(e.Date)})]},`${e.PTS}-${e.Type}`)),i.UpdateEvents.length===0&&(0,V.jsx)(Je,{colSpan:4})]})]})})]}),(0,V.jsxs)(`section`,{className:`section-block`,children:[(0,V.jsx)(Ge,{title:r(`messages.dispatchOutbox`),text:r(`messages.outboxSource`)}),(0,V.jsx)(`div`,{className:`table-wrap`,children:(0,V.jsxs)(`table`,{className:`data-table`,children:[(0,V.jsx)(`thead`,{children:(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`th`,{children:`ID`}),(0,V.jsx)(`th`,{children:r(`account.userID`)}),(0,V.jsx)(`th`,{children:`PTS`}),(0,V.jsx)(`th`,{children:r(`common.type`)}),(0,V.jsx)(`th`,{children:r(`common.status`)}),(0,V.jsx)(`th`,{children:r(`messages.attempts`)}),(0,V.jsx)(`th`,{children:r(`common.updatedAt`)})]})}),(0,V.jsxs)(`tbody`,{children:[i.Outbox.map(e=>(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`td`,{children:e.ID}),(0,V.jsx)(`td`,{children:e.TargetUserID}),(0,V.jsx)(`td`,{children:e.PTS}),(0,V.jsx)(`td`,{children:e.EventType}),(0,V.jsx)(`td`,{children:e.Status}),(0,V.jsx)(`td`,{children:e.Attempts}),(0,V.jsx)(`td`,{children:ze(e.UpdatedAt)})]},e.ID)),i.Outbox.length===0&&(0,V.jsx)(Je,{colSpan:7})]})]})})]})]}),side:(0,V.jsxs)(`section`,{className:`action-dock`,children:[(0,V.jsx)(`div`,{className:`dock-title`,children:r(`common.operations`)}),(0,V.jsx)($e,{label:r(`messages.deleteThis`),icon:(0,V.jsx)(ge,{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 ht({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,V.jsxs)(He,{title:t(`messages.privateTitle`),eyebrow:t(`messages.privateEyebrow`),children:[D&&(0,V.jsx)(Ke,{children:D}),(0,V.jsxs)(Ue,{children:[(0,V.jsxs)(`div`,{className:`message-selector-grid`,children:[(0,V.jsx)(dt,{label:t(`messages.ownerUser`),value:n,onChange:A}),(0,V.jsx)(dt,{label:t(`messages.peerUser`),value:i,onChange:j})]}),(0,V.jsxs)(`form`,{className:`toolbar message-query`,onSubmit:e=>{e.preventDefault(),k(!1)},children:[(0,V.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:t(`messages.beforeDatePlaceholder`)}),(0,V.jsx)(`input`,{value:c,onChange:e=>l(e.target.value),placeholder:t(`messages.beforeIDPlaceholder`)}),(0,V.jsx)(`input`,{className:`small-input`,value:u,onChange:e=>d(e.target.value),placeholder:t(`messages.limitPlaceholder`)}),(0,V.jsxs)(`button`,{className:`btn primary icon-text`,type:`submit`,children:[(0,V.jsx)(B,{size:15}),` `,t(`messages.searchMessages`)]}),T?.rows.length?(0,V.jsxs)(`button`,{className:`btn icon-text`,type:`button`,onClick:()=>k(!0),children:[(0,V.jsx)(I,{size:15}),` `,t(`messages.nextPage`)]}):null]})]}),(0,V.jsxs)(`div`,{className:`metric-row`,children:[(0,V.jsx)(q,{label:t(`messages.currentPage`),value:String(T?.rows.length??0)}),(0,V.jsx)(q,{label:t(`messages.deleted`),value:String((T?.rows??[]).filter(e=>e.Deleted).length),tone:`danger`}),(0,V.jsx)(q,{label:t(`messages.outgoing`),value:String((T?.rows??[]).filter(e=>e.Outgoing).length)}),(0,V.jsx)(q,{label:t(`messages.ownerPeer`),value:n&&i?`${Le(n)} / ${Le(i)}`:`-`})]}),(0,V.jsxs)(`div`,{className:`operation-row`,children:[(0,V.jsxs)(`div`,{className:`operation-box`,children:[(0,V.jsxs)(`div`,{className:`operation-title`,children:[(0,V.jsx)(ge,{size:15}),` `,t(`messages.deleteSelected`)]}),(0,V.jsx)(`input`,{value:f,onChange:e=>p(e.target.value),placeholder:t(`messages.idsPlaceholder`)}),(0,V.jsxs)(`label`,{className:`checkline`,children:[(0,V.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,V.jsx)($e,{path:`/api/actions/delete-messages`,label:t(`messages.previewDelete`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,ids:Ve(f,t(`messages.msgIDsInvalid`)),revoke:m})})]}),(0,V.jsxs)(`div`,{className:`operation-box`,children:[(0,V.jsxs)(`div`,{className:`operation-title`,children:[(0,V.jsx)(ie,{size:15}),` `,t(`messages.clearHistory`)]}),(0,V.jsx)(`input`,{value:y,onChange:e=>S(e.target.value),placeholder:t(`messages.maxIDPlaceholder`)}),(0,V.jsx)(`input`,{value:C,onChange:e=>w(e.target.value),placeholder:t(`messages.maxBatchesPlaceholder`)}),(0,V.jsxs)(`label`,{className:`checkline`,children:[(0,V.jsx)(`input`,{type:`checkbox`,checked:m,onChange:e=>h(e.target.checked)}),` `,t(`messages.revoke`)]}),(0,V.jsxs)(`label`,{className:`checkline`,children:[(0,V.jsx)(`input`,{type:`checkbox`,checked:_,onChange:e=>v(e.target.checked)}),` `,t(`messages.justClear`)]}),(0,V.jsx)($e,{path:`/api/actions/delete-history`,label:t(`messages.previewClearHistory`),payload:()=>({owner_user_id:n?.ID??0,peer_id:i?.ID??0,max_id:W(y),max_batches:W(C),just_clear:_,revoke:m})})]})]}),(0,V.jsx)(`div`,{className:`table-wrap`,children:(0,V.jsxs)(`table`,{className:`data-table`,children:[(0,V.jsx)(`thead`,{children:(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`th`,{children:t(`common.messageId`)}),(0,V.jsx)(`th`,{children:t(`common.time`)}),(0,V.jsx)(`th`,{children:t(`common.sender`)}),(0,V.jsx)(`th`,{children:t(`messages.direction`)}),(0,V.jsx)(`th`,{children:`PTS`}),(0,V.jsx)(`th`,{children:t(`common.status`)}),(0,V.jsx)(`th`,{children:t(`messages.body`)}),(0,V.jsx)(`th`,{})]})}),(0,V.jsxs)(`tbody`,{children:[T?.rows.map(n=>(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`td`,{className:`mono`,children:n.BoxID}),(0,V.jsx)(`td`,{children:Be(n.Date)}),(0,V.jsx)(`td`,{className:`mono`,children:n.FromUserID}),(0,V.jsx)(`td`,{children:n.Outgoing?t(`messages.outgoing`):t(`messages.incoming`)}),(0,V.jsx)(`td`,{children:n.PTS}),(0,V.jsx)(`td`,{children:n.Deleted?(0,V.jsx)(G,{tone:`danger`,children:t(`common.deleted`)}):(0,V.jsx)(G,{children:t(`common.survived`)})}),(0,V.jsx)(`td`,{className:`truncate`,children:n.Body}),(0,V.jsx)(`td`,{children:(0,V.jsxs)(`button`,{className:`row-link`,onClick:()=>e(`/messages/private/detail?owner_user_id=${n.OwnerUserID}&msg_id=${n.BoxID}`),children:[t(`common.detail`),` `,(0,V.jsx)(I,{size:14})]})})]},`${n.OwnerUserID}-${n.BoxID}`)),(!T||T.rows.length===0)&&(0,V.jsx)(Je,{colSpan:8})]})]})})]})}var gt=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}(),Se=function(){function e(e){return e.concat(m(e.length))}return{double:e}}(),Ce=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=Se.double(a),i*=2),n&&n(e),a[r]=e,r+=1}return o}}(),U=function(){function e(){return{addedLength:0,percents:p(`float32`,le()),lengths:p(`float32`,le())}}return Ce(8,e)}(),we=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=U.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=Me(c.s),M=Me(b),N=(e-y)/(v-y);je(r,Ae(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 je(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 Me(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 Ne(){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!==De&&(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 Pe(e){var t;if(this.propType===`unidimensional`)t=e*this.mult,Oe(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 Fe(){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=xe.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 q(e){"@babel/helpers - typeof";return q=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},q(e)}var J={},qe=`__[STANDALONE]__`,Je=`__[ANIMATIONDATA]__`,Ye=``;function Xe(e){s(e)}function Ze(){qe===!0?H.searchAnimations(Je,qe,Ye):H.searchAnimations()}function Qe(e){re(e)}function $e(e){ue(e)}function et(e){return qe===!0&&(e.animationData=JSON.parse(Je)),H.loadAnimation(e)}function tt(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 nt(){return typeof navigator<`u`}function rt(e,t){e===`expressions`&&ae(t)}function it(e){switch(e){case`propertyFactory`:return W;case`shapePropertyFactory`:return G;case`matrix`:return K;default:return null}}J.play=H.play,J.pause=H.pause,J.setLocationHref=Xe,J.togglePause=H.togglePause,J.setSpeed=H.setSpeed,J.setDirection=H.setDirection,J.stop=H.stop,J.searchAnimations=Ze,J.registerAnimation=H.registerAnimation,J.loadAnimation=et,J.setSubframeRendering=Qe,J.resize=H.resize,J.goToAndStop=H.goToAndStop,J.destroy=H.destroy,J.setQuality=tt,J.inBrowser=nt,J.installPlugin=rt,J.freeze=H.freeze,J.unfreeze=H.unfreeze,J.setVolume=H.setVolume,J.mute=H.mute,J.unmute=H.unmute,J.getRegisteredAnimations=H.getRegisteredAnimations,J.useWebWorker=a,J.setIDPrefix=$e,J.__getFactory=it,J.version=`5.13.0`;function at(){document.readyState===`complete`&&(clearInterval(ut),Ze())}function ot(e){for(var t=st.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},pt.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=Ee.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=Ee.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 K,this.pre=new K,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)},gt.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})},Tt.prototype.split=function(e){if(e<=0)return[wt(this.points[0]),this];if(e>=1)return[this,wt(this.points[this.points.length-1])];var t=xt(this.points[0],this.points[1],e),n=xt(this.points[1],this.points[2],e),r=xt(this.points[2],this.points[3],e),i=xt(t,n,e),a=xt(n,r,e),o=xt(i,a,e);return[new Tt(this.points[0],t,i,o,!0),new Tt(o,a,r,this.points[3],!0)]};function Et(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=St(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}}Tt.prototype.bounds=function(){return{x:Et(this,0),y:Et(this,1)}},Tt.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 Dt(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 Ot(e){var t=e.bez.split(.5);return[Dt(t[0],e.t1,e.t),Dt(t[1],e.t,e.t2)]}function kt(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=Ot(e),s=Ot(t);At(o[0],s[0],n+1,r,i,a),At(o[0],s[1],n+1,r,i,a),At(o[1],s[0],n+1,r,i,a),At(o[1],s[1],n+1,r,i,a)}}Tt.prototype.intersections=function(e,t,n){t===void 0&&(t=2),n===void 0&&(n=7);var r=[];return At(Dt(this,0,1),Dt(e,0,1),0,t,r,n),r},Tt.shapeSegment=function(e,t){var n=(t+1)%e.length();return new Tt(e.v[t],e.o[t],e.i[n],e.v[n],!0)},Tt.shapeSegmentInverted=function(e,t){var n=(t+1)%e.length();return new Tt(e.v[n],e.i[n],e.o[t],e.v[t],!0)};function jt(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 Mt(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=jt(jt(i,a),jt(o,s));return yt(c[2])?null:[c[0]/c[2],c[1]/c[2]]}function Y(e,t,n){return[e[0]+Math.cos(t)*n,e[1]-Math.sin(t)*n]}function Nt(e,t){return Math.hypot(e[0]-t[0],e[1]-t[1])}function Pt(e,t){return vt(e[0],t[0])&&vt(e[1],t[1])}function Ft(){}u([ft],Ft),Ft.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 It(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 Lt(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 Rt(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=Lt(i,a);return Math.atan2(0,1)-Math.atan2(o[1],o[0])}function zt(e,t,n,r,i,a,o){var s=Rt(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;It(e,t.v[n%t._length],s,o,r,f/((i+1)*2),d/((i+1)*2),a)}function Bt(e,t,n,r,i,a){for(var o=0;o1&&t.length>1&&(i=Wt(e[0],t[t.length-1]),i)?[[e[0].split(i[0])[0]],[t[t.length-1].split(i[1])[1]]]:[n,r]}function Kt(e){for(var t,n=1;n1&&(t=Gt(e[e.length-1],e[0]),e[e.length-1]=t[0],e[0]=t[1]),e}function qt(e,t){var n=e.inflectionPoints(),r,i,a,o;if(n.length===0)return[Ht(e,t)];if(n.length===1||vt(n[1],1))return a=e.split(n[0]),r=a[0],i=a[1],[Ht(r,t),Ht(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],[Ht(r,t),Ht(o,t),Ht(i,t)]}function Jt(){}u([ft],Jt),Jt.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},Jt.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=Tt.shapeSegmentInverted(e,o),l.push(qt(c,t));l=Kt(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 Zt(e){this.animationData=e}Zt.prototype.getProp=function(e){return this.animationData.slots&&this.animationData.slots[e.sid]?Object.assign(e,this.animationData.slots[e.sid].p):e};function Qt(e){return new Zt(e)}function $t(){}$t.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))},hn.prototype.show=function(){},hn.prototype.hide=function(){this.audio.pause(),this._isPlaying=!1},hn.prototype.pause=function(){this.audio.pause(),this._isPlaying=!1,this._canPlay=!1},hn.prototype.resume=function(){this._canPlay=!0},hn.prototype.setRate=function(e){this.audio.rate(e)},hn.prototype.volume=function(e){this._volumeMultiplier=e,this._previousVolume=e*this._volume,this.audio.volume(this._previousVolume)},hn.prototype.getBaseElement=function(){return null},hn.prototype.destroy=function(){},hn.prototype.sourceRectAtTime=function(){},hn.prototype.initExpressions=function(){};function gn(){}gn.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()},gn.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)}},gn.prototype.createCamera=function(){throw Error(`You're using a 3d camera. Try the html renderer.`)},gn.prototype.createAudio=function(e){return new hn(e,this.globalData,this)},gn.prototype.createFootage=function(e){return new mn(e,this.globalData,this)},gn.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)}yn.prototype.getMaskProperty=function(e){return this.viewData[e].prop},yn.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}},yn.prototype.destroy=function(){this.element=null,this.globalData=null,this.maskElement=null,this.data=null,this.masksProperties=null};var bn=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}(),xn=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}(),Sn={},Cn=`filter_result_`;function wn(e){var t,n=`SourceGraphic`,r=e.data.ef?e.data.ef.length:0,i=I(),a=bn.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},zn.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 X(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([fn,vn,Tn,An,En,pn,Dn],X),X.prototype.initSecondaryElement=function(){},X.prototype.identityMatrix=new K,X.prototype.buildExpressionInterface=function(){},X.prototype.createContent=function(){this.searchShapes(this.shapesData,this.itemsData,this.prevViewData,this.layerElement,0,[],!0),this.filterUniqueShapes()},X.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)}},X.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?Xt.isRegionalFlag(e,n)?c=e.substr(n,14):(a=e.charCodeAt(n+1),a>=56320&&a<=57343&&(Xt.isModifier(i,a)?(c=e.substr(n,2),o=!0):c=Xt.isFlagEmoji(e.substr(n,4))?e.substr(n,4):e.substr(n,2))):i>56319?(a=e.charCodeAt(n+1),Xt.isVariationSelector(i)&&(o=!0)):Xt.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=Yt(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=xe.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+Se||!x?(T=(m+Se-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 X(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=en(this.data.bm);e.canvasContext.globalCompositeOperation=t}},createRenderableComponents:function(){this.maskManager=new fr(this.data,this),this.transformEffects=this.renderableEffectsManager.getEffects(_n.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 K},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=G.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 K;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 K,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),dt.registerModifier(`tm`,pt),dt.registerModifier(`pb`,mt),dt.registerModifier(`rp`,gt),dt.registerModifier(`rd`,_t),dt.registerModifier(`zz`,Ft),dt.registerModifier(`op`,Jt),J}))}))(),1),_t=0,vt=e=>`${e}-${++_t}`,yt=e=>({key:vt(e),name:``,rarity:`1000`,sortOrder:`0`,file:null,animation:null,fileError:``}),bt=()=>({key:vt(`backdrop`),name:``,backdropID:`1`,rarity:`1000`,sortOrder:`0`,center:`#6f5bea`,edge:`#34278f`,pattern:`#a89df5`,text:`#ffffff`});function xt({data:e,compact:t=!1}){let n=(0,g.useRef)(null);return(0,g.useEffect)(()=>{if(!n.current)return;let t=gt.default.loadAnimation({container:n.current,renderer:`canvas`,loop:!0,autoplay:!0,animationData:structuredClone(e)});return()=>t.destroy()},[e]),(0,V.jsx)(`div`,{className:`collectible-animation ${t?`compact`:``}`,ref:n})}function St({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,V.jsx)(`div`,{className:`collectible-animation compact failed`,children:`!`}):n?(0,V.jsx)(xt,{data:n,compact:!0}):(0,V.jsx)(`div`,{className:`collectible-animation compact loading`,children:(0,V.jsx)(A,{className:`spin`,size:15})})}async function Ct(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 wt=e=>Number.parseInt(e.replace(`#`,``),16);function Tt({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)([yt(`model`)]),[D,O]=(0,g.useState)([yt(`pattern`)]),[M,N]=(0,g.useState)([bt()]);(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:M.reduce((e,t)=>e+Number(t.rarity||0),0)}),[T,D,M]),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 Ct(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`));for(let e of[...T,...D])if(!e.file)throw Error(r(`collectibles.fileRequired`));let n=new FormData,i=e=>e.map(e=>({name:e.name.trim(),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),file_key:e.key}));n.set(`metadata`,JSON.stringify({command_id:t,reason:C.trim(),confirm:e,upgrade_stars:Number(m),supply_total:Number(_),slug_prefix:y.trim().toLowerCase(),models:i(T),patterns:i(D),backdrops:M.map(e=>({name:e.name.trim(),backdrop_id:Number(e.backdropID),rarity_permille:Number(e.rarity),sort_order:Number(e.sortOrder),center_color:wt(e.center),edge_color:wt(e.edge),pattern_color:wt(e.pattern),text_color:wt(e.text)}))}));for(let e of[...T,...D])n.set(e.key,e.file,e.file.name);return n}async function te(){l(!0),d(``),p(null);try{p(await x.publishGiftCollectibles(e.GiftID,ee(!1)))}catch(e){d(b(e))}finally{l(!1)}}async function re(){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 ie=(e,t,n)=>(0,V.jsxs)(`section`,{className:`collectible-section`,children:[(0,V.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`strong`,{children:r(`collectibles.${e}`)}),(0,V.jsx)(`span`,{children:r(`collectibles.rarityHint`)})]}),(0,V.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,V.jsxs)(G,{tone:P[e]===1e3?`good`:`neutral`,children:[P[e],` / 1000`]}),(0,V.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{n([...t,yt(e===`models`?`model`:`pattern`)]),F()},children:[(0,V.jsx)(z,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,V.jsx)(`div`,{className:`collectible-rows`,children:t.map((i,a)=>(0,V.jsxs)(`div`,{className:`collectible-row animated`,children:[(0,V.jsx)(`div`,{className:`collectible-row-index`,children:a+1}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:r(`common.name`)}),(0,V.jsx)(`input`,{value:i.name,maxLength:128,onChange:t=>I(e,i.key,{name:t.target.value})})]}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,V.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:i.rarity,onChange:t=>I(e,i.key,{rarity:t.target.value})})]}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,V.jsx)(`input`,{type:`number`,value:i.sortOrder,onChange:t=>I(e,i.key,{sortOrder:t.target.value})})]}),(0,V.jsxs)(`label`,{className:`collectible-file`,children:[(0,V.jsx)(`span`,{children:r(`gifts.animation`)}),(0,V.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,V.jsxs)(`em`,{children:[(0,V.jsx)(R,{size:13}),i.file?.name??r(`gifts.chooseFile`)]})]}),(0,V.jsx)(`div`,{className:`collectible-inline-preview`,children:i.animation?(0,V.jsx)(xt,{data:i.animation,compact:!0}):(0,V.jsx)(j,{size:16})}),(0,V.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:t.length===1,onClick:()=>{n(t.filter(e=>e.key!==i.key)),F()},"aria-label":r(`collectibles.remove`),children:(0,V.jsx)(ge,{size:14})}),i.fileError&&(0,V.jsx)(`span`,{className:`collectible-file-error`,children:i.fileError})]},i.key))})]});return(0,Qe.createPortal)((0,V.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,V.jsxs)(`section`,{className:`modal command-modal collectible-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":r(`collectibles.title`,{id:e.GiftID}),children:[(0,V.jsxs)(`div`,{className:`modal-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`div`,{className:`eyebrow`,children:r(`collectibles.eyebrow`)}),(0,V.jsx)(`h2`,{children:r(`collectibles.title`,{id:e.GiftID})}),(0,V.jsx)(`p`,{children:e.Title||`Gift #${e.GiftID}`})]}),(0,V.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:t,disabled:c,"aria-label":r(`action.close`),children:(0,V.jsx)(ye,{size:15})})]}),(0,V.jsxs)(`div`,{className:`command-body collectible-modal-body`,children:[o?(0,V.jsxs)(`div`,{className:`collectible-loading`,children:[(0,V.jsx)(A,{className:`spin`}),r(`common.loading`)]}):i?.found?(0,V.jsxs)(`section`,{className:`collectible-active`,children:[(0,V.jsxs)(`div`,{className:`collectible-active-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(ne,{size:18}),(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`strong`,{children:r(`collectibles.activeRevision`,{revision:i.revision??0})}),(0,V.jsxs)(`span`,{children:[i.slug_prefix,` · ⭐ `,i.upgrade_stars,` · `,i.issued,` / `,i.supply_total]})]})]}),(0,V.jsx)(G,{tone:`good`,children:r(`collectibles.published`)})]}),(0,V.jsxs)(`div`,{className:`collectible-active-grid`,children:[[...i.models??[],...i.patterns??[]].map(t=>(0,V.jsxs)(`article`,{children:[(0,V.jsx)(St,{giftID:e.GiftID,attribute:t}),(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`strong`,{children:t.name}),(0,V.jsxs)(`span`,{children:[r(`collectibles.${t.kind}`),` · `,t.rarity_permille,`‰`]})]})]},`${t.kind}-${t.id}`)),(i.backdrops??[]).map(e=>(0,V.jsxs)(`article`,{children:[(0,V.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,V.jsxs)(`div`,{children:[(0,V.jsx)(`strong`,{children:e.name}),(0,V.jsxs)(`span`,{children:[r(`collectibles.backdrop`),` · `,e.rarity_permille,`‰`]})]})]},`backdrop-${e.id}`))]})]}):(0,V.jsxs)(`div`,{className:`collectible-empty`,children:[(0,V.jsx)(ne,{size:22}),(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`strong`,{children:r(`collectibles.noPool`)}),(0,V.jsx)(`span`,{children:r(`collectibles.noPoolHint`)})]})]}),(0,V.jsxs)(`section`,{className:`collectible-definition`,children:[(0,V.jsxs)(`div`,{className:`collectible-definition-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`strong`,{children:r(`collectibles.publishNew`)}),(0,V.jsx)(`span`,{children:r(`collectibles.immutableHint`)})]}),(0,V.jsxs)(`div`,{className:`gift-format-chips`,children:[(0,V.jsx)(`span`,{children:`TGS`}),(0,V.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,V.jsxs)(`div`,{className:`gift-fields-grid collectible-main-fields`,children:[(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:r(`collectibles.upgradeStars`)}),(0,V.jsx)(`input`,{type:`number`,min:`1`,value:m,onChange:e=>{h(e.target.value),F()}})]}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:r(`collectibles.supply`)}),(0,V.jsx)(`input`,{type:`number`,min:`1`,value:_,onChange:e=>{v(e.target.value),F()}})]}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:r(`collectibles.slug`)}),(0,V.jsx)(`input`,{value:y,maxLength:48,onChange:e=>{S(e.target.value.toLowerCase()),F()}})]}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:r(`gifts.reason`)}),(0,V.jsx)(`input`,{value:C,maxLength:1e3,placeholder:r(`gifts.reasonPlaceholder`),onChange:e=>w(e.target.value)})]})]}),ie(`models`,T,E),ie(`patterns`,D,O),(0,V.jsxs)(`section`,{className:`collectible-section`,children:[(0,V.jsxs)(`div`,{className:`collectible-section-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`strong`,{children:r(`collectibles.backdrops`)}),(0,V.jsx)(`span`,{children:r(`collectibles.colorHint`)})]}),(0,V.jsxs)(`div`,{className:`collectible-section-tools`,children:[(0,V.jsxs)(G,{tone:P.backdrops===1e3?`good`:`neutral`,children:[P.backdrops,` / 1000`]}),(0,V.jsxs)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>{N([...M,bt()]),F()},children:[(0,V.jsx)(z,{size:13}),r(`collectibles.addAttribute`)]})]})]}),(0,V.jsx)(`div`,{className:`collectible-rows`,children:M.map((e,t)=>(0,V.jsxs)(`div`,{className:`collectible-row backdrop`,children:[(0,V.jsx)(`div`,{className:`collectible-row-index`,children:t+1}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:r(`common.name`)}),(0,V.jsx)(`input`,{value:e.name,maxLength:128,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,name:t.target.value}:n)),F()}})]}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:r(`collectibles.backdropID`)}),(0,V.jsx)(`input`,{type:`number`,min:`1`,value:e.backdropID,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,backdropID:t.target.value}:n)),F()}})]}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:r(`collectibles.rarity`)}),(0,V.jsx)(`input`,{type:`number`,min:`1`,max:`1000`,value:e.rarity,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,rarity:t.target.value}:n)),F()}})]}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:r(`gifts.sortOrder`)}),(0,V.jsx)(`input`,{type:`number`,value:e.sortOrder,onChange:t=>{N(M.map(n=>n.key===e.key?{...n,sortOrder:t.target.value}:n)),F()}})]}),[`center`,`edge`,`pattern`,`text`].map(t=>(0,V.jsxs)(`label`,{className:`collectible-color`,children:[(0,V.jsx)(`span`,{children:r(`collectibles.color.${t}`)}),(0,V.jsx)(`input`,{type:`color`,value:e[t],onChange:n=>{N(M.map(r=>r.key===e.key?{...r,[t]:n.target.value}:r)),F()}})]},t)),(0,V.jsx)(`div`,{className:`collectible-backdrop-preview`,style:{background:`radial-gradient(circle, ${e.center}, ${e.edge})`,color:e.text},children:`Aa`}),(0,V.jsx)(`button`,{className:`icon-btn danger`,type:`button`,disabled:M.length===1,onClick:()=>{N(M.filter(t=>t.key!==e.key)),F()},"aria-label":r(`collectibles.remove`),children:(0,V.jsx)(ge,{size:14})})]},e.key))})]})]}),u&&(0,V.jsx)(Ke,{children:u}),f&&(0,V.jsxs)(`div`,{className:`gift-validation`,children:[(0,V.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,V.jsx)(k,{size:17}),(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`strong`,{children:r(`collectibles.validationReady`)}),(0,V.jsx)(`span`,{children:r(`collectibles.validationHint`)})]})]}),(0,V.jsx)(`pre`,{children:JSON.stringify(f.details,null,2)})]})]}),(0,V.jsxs)(`div`,{className:`modal-actions`,children:[(0,V.jsx)(`button`,{className:`btn`,type:`button`,onClick:t,disabled:c,children:r(`common.close`)}),(0,V.jsxs)(`button`,{className:`btn`,type:`button`,onClick:te,disabled:c,children:[c?(0,V.jsx)(A,{className:`spin`,size:15}):(0,V.jsx)(pe,{size:15}),r(`gifts.validate`)]}),(0,V.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:re,disabled:c||!f,children:[(0,V.jsx)(_e,{size:15}),r(`collectibles.publish`)]})]})]})}),document.body)}function Et(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function Dt({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=gt.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,V.jsxs)(`div`,{className:`gift-animation-shell ${n?`compact`:``}`,children:[(0,V.jsx)(`div`,{className:`gift-animation`,ref:r,children:s&&(0,V.jsx)(`span`,{children:s})}),(0,V.jsx)(`button`,{className:`gift-play`,type:`button`,onClick:l,"aria-label":a?`Pause`:`Play`,children:a?(0,V.jsx)(le,{size:14}):(0,V.jsx)(ue,{size:14})})]})}function Ot(){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)(0),[p,m]=(0,g.useState)(``),[h,_]=(0,g.useState)(`50`),[v,y]=(0,g.useState)(`50`),[S,C]=(0,g.useState)(`0`),[w,T]=(0,g.useState)(!0),[E,D]=(0,g.useState)(``),[O,j]=(0,g.useState)(null),[M,N]=(0,g.useState)(!1),[P,F]=(0,g.useState)(``),[I,L]=(0,g.useState)(``);async function ee(){F(``);try{n((await x.gifts()).Gifts??[])}catch(e){F(b(e))}}(0,g.useEffect)(()=>{ee()},[]);let te=(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 re(t,n=``){if(!l)throw Error(e(`gifts.fileRequired`));if(!E.trim())throw Error(e(`action.reasonRequired`));let r=new FormData;return r.set(`metadata`,JSON.stringify({command_id:n,reason:E.trim(),confirm:t,gift_id:d,title:p.trim(),stars:Number(h),convert_stars:Number(v),enabled:w,sort_order:Number(S)})),r.set(`file`,l,l.name),r}async function ie(){N(!0),L(``),j(null);try{j(await x.importGift(re(!1)))}catch(e){L(b(e))}finally{N(!1)}}async function ae(){if(O){N(!0),L(``);try{await x.importGift(re(!0,O.command_id)),j(null),u(null),f(0),m(``),await ee(),o(!1)}catch(e){L(b(e))}finally{N(!1)}}}function oe(){f(0),m(``),_(`50`),y(`50`),C(`0`),T(!0),D(``),u(null),j(null),L(``),o(!0)}function se(e){f(e.GiftID),m(e.Title),_(String(e.Stars)),y(String(e.ConvertStars)),C(String(e.SortOrder)),T(e.Enabled),D(``),u(null),j(null),L(``),o(!0)}return(0,V.jsxs)(He,{title:e(`gifts.pageTitle`),eyebrow:e(`gifts.eyebrow`),actions:(0,V.jsxs)(V.Fragment,{children:[(0,V.jsxs)(`button`,{className:`btn`,type:`button`,onClick:()=>ee(),disabled:M,children:[(0,V.jsx)(de,{size:15}),` `,e(`common.refresh`)]}),(0,V.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:oe,children:[(0,V.jsx)(z,{size:15}),` `,e(`gifts.add`)]})]}),children:[P&&(0,V.jsx)(Ke,{children:P}),(0,V.jsxs)(`div`,{className:`metric-row gift-metrics`,children:[(0,V.jsx)(q,{label:e(`gifts.total`),value:String(t.length)}),(0,V.jsx)(q,{label:e(`gifts.enabled`),value:String(t.filter(e=>e.Enabled).length),tone:`good`}),(0,V.jsx)(q,{label:e(`gifts.received`),value:String(t.reduce((e,t)=>e+t.ReceivedCount,0))}),(0,V.jsx)(q,{label:e(`gifts.formats`),value:`TGS / Lottie`})]}),(0,V.jsx)(Ue,{children:(0,V.jsxs)(`div`,{className:`toolbar`,children:[(0,V.jsxs)(`label`,{className:`searchbox`,children:[(0,V.jsx)(B,{size:15}),(0,V.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:e(`gifts.searchPlaceholder`)})]}),(0,V.jsx)(`span`,{className:`gift-list-summary`,children:e(`gifts.listSummary`,{shown:te.length,total:t.length})})]})}),(0,V.jsx)(`div`,{className:`table-wrap gift-table-wrap`,children:(0,V.jsxs)(`table`,{className:`data-table gift-table`,children:[(0,V.jsx)(`thead`,{children:(0,V.jsxs)(`tr`,{children:[(0,V.jsx)(`th`,{children:e(`gifts.animation`)}),(0,V.jsx)(`th`,{children:e(`gifts.idRevision`)}),(0,V.jsx)(`th`,{children:e(`gifts.title`)}),(0,V.jsx)(`th`,{children:e(`gifts.price`)}),(0,V.jsx)(`th`,{children:e(`gifts.source`)}),(0,V.jsx)(`th`,{children:e(`gifts.received`)}),(0,V.jsx)(`th`,{children:e(`common.status`)}),(0,V.jsx)(`th`,{children:e(`common.updatedAt`)}),(0,V.jsx)(`th`,{children:e(`common.actions`)})]})}),(0,V.jsxs)(`tbody`,{children:[te.map(t=>(0,V.jsxs)(`tr`,{className:t.Enabled?``:`gift-row-disabled`,children:[(0,V.jsx)(`td`,{children:(0,V.jsx)(Dt,{giftID:t.GiftID,revision:t.Revision,compact:!0})}),(0,V.jsxs)(`td`,{className:`mono`,children:[t.GiftID,` / `,t.Revision]}),(0,V.jsxs)(`td`,{children:[(0,V.jsx)(`strong`,{className:`gift-table-title`,children:t.Title||`Gift #${t.GiftID}`}),(0,V.jsxs)(`span`,{className:`gift-sort-order`,children:[e(`gifts.sortOrder`),`: `,t.SortOrder]})]}),(0,V.jsxs)(`td`,{children:[(0,V.jsxs)(`strong`,{className:`gift-table-price`,children:[`⭐ `,t.Stars]}),(0,V.jsxs)(`span`,{className:`gift-convert-price`,children:[`→ `,t.ConvertStars]})]}),(0,V.jsxs)(`td`,{children:[(0,V.jsx)(G,{children:t.SourceFormat}),(0,V.jsx)(`span`,{className:`gift-source-size`,children:Et(t.AnimationSize)})]}),(0,V.jsx)(`td`,{children:t.ReceivedCount}),(0,V.jsx)(`td`,{children:(0,V.jsx)(G,{tone:t.Enabled?`good`:`neutral`,children:t.Enabled?e(`common.enabled`):e(`common.disabled`)})}),(0,V.jsx)(`td`,{children:ze(t.UpdatedAt)}),(0,V.jsx)(`td`,{children:(0,V.jsxs)(`div`,{className:`gift-table-actions`,children:[(0,V.jsxs)(`button`,{className:`btn compact-btn collectible-button`,type:`button`,onClick:()=>c(t),children:[(0,V.jsx)(ne,{size:13}),e(`collectibles.manage`)]}),(0,V.jsx)(`button`,{className:`btn compact-btn`,type:`button`,onClick:()=>se(t),children:e(`gifts.replace`)}),(0,V.jsx)($e,{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 ee()})]})})]},t.GiftID)),te.length===0&&(0,V.jsx)(Je,{colSpan:9})]})]})}),a&&(0,Qe.createPortal)((0,V.jsx)(`div`,{className:`modal-backdrop`,role:`presentation`,children:(0,V.jsxs)(`section`,{className:`modal command-modal gift-import-modal`,role:`dialog`,"aria-modal":`true`,"aria-label":d?e(`gifts.newRevision`,{id:d}):e(`gifts.importTitle`),children:[(0,V.jsxs)(`div`,{className:`modal-head`,children:[(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`div`,{className:`eyebrow`,children:e(`gifts.importEyebrow`)}),(0,V.jsx)(`h2`,{children:d?e(`gifts.newRevision`,{id:d}):e(`gifts.importTitle`)})]}),(0,V.jsx)(`button`,{className:`icon-btn`,type:`button`,onClick:()=>o(!1),disabled:M,"aria-label":e(`action.close`),children:(0,V.jsx)(ye,{size:15})})]}),(0,V.jsxs)(`div`,{className:`command-body gift-import-modal-body`,children:[(0,V.jsxs)(`div`,{className:`command-steps`,children:[(0,V.jsxs)(`div`,{className:`command-step ${l?`done`:`active`}`,children:[(0,V.jsx)(`span`,{children:`1`}),(0,V.jsx)(`strong`,{children:e(`gifts.stepDetails`)})]}),(0,V.jsxs)(`div`,{className:`command-step ${O?`done`:l?`active`:``}`,children:[(0,V.jsx)(`span`,{children:`2`}),(0,V.jsx)(`strong`,{children:e(`gifts.stepValidate`)})]}),(0,V.jsxs)(`div`,{className:`command-step ${O?`active`:``}`,children:[(0,V.jsx)(`span`,{children:`3`}),(0,V.jsx)(`strong`,{children:e(`gifts.stepImport`)})]})]}),(0,V.jsxs)(`div`,{className:`gift-import-note`,children:[(0,V.jsx)(`span`,{children:e(`gifts.importHint`)}),(0,V.jsxs)(`div`,{className:`gift-format-chips`,"aria-label":e(`gifts.formats`),children:[(0,V.jsx)(`span`,{children:`TGS`}),(0,V.jsx)(`span`,{children:`Lottie JSON`})]})]}),(0,V.jsxs)(`label`,{className:`gift-file-picker ${l?`has-file`:``}`,children:[(0,V.jsx)(`input`,{type:`file`,accept:`.tgs,.json,.lottie,application/json,application/x-tgsticker`,onChange:e=>{u(e.target.files?.[0]??null),j(null)}}),(0,V.jsx)(`span`,{className:`gift-file-icon`,children:(0,V.jsx)(R,{size:22})}),(0,V.jsxs)(`span`,{className:`gift-file-copy`,children:[(0,V.jsx)(`span`,{className:`gift-field-label`,children:e(`gifts.animation`)}),(0,V.jsx)(`strong`,{children:l?l.name:e(`gifts.filePrompt`)}),(0,V.jsx)(`small`,{children:l?Et(l.size):e(`gifts.fileHint`)})]}),(0,V.jsx)(`span`,{className:`gift-file-action`,children:e(l?`gifts.changeFile`:`gifts.chooseFile`)})]}),(0,V.jsxs)(`div`,{className:`gift-fields-grid`,children:[(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:e(`gifts.title`)}),(0,V.jsx)(`input`,{value:p,maxLength:128,placeholder:e(`gifts.titlePlaceholder`),onChange:e=>{m(e.target.value),j(null)}})]}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:e(`gifts.stars`)}),(0,V.jsx)(`input`,{type:`number`,min:`1`,value:h,onChange:e=>{_(e.target.value),j(null)}})]}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:e(`gifts.convertStars`)}),(0,V.jsx)(`input`,{type:`number`,min:`0`,value:v,onChange:e=>{y(e.target.value),j(null)}})]}),(0,V.jsxs)(`label`,{children:[(0,V.jsx)(`span`,{children:e(`gifts.sortOrder`)}),(0,V.jsx)(`input`,{type:`number`,value:S,onChange:e=>{C(e.target.value),j(null)}})]})]}),(0,V.jsxs)(`label`,{className:`gift-reason-field`,children:[(0,V.jsx)(`span`,{children:e(`gifts.reason`)}),(0,V.jsx)(`input`,{value:E,placeholder:e(`gifts.reasonPlaceholder`),onChange:e=>D(e.target.value)})]}),(0,V.jsxs)(`label`,{className:`gift-switch`,children:[(0,V.jsx)(`input`,{type:`checkbox`,checked:w,onChange:e=>{T(e.target.checked),j(null)}}),(0,V.jsx)(`span`,{className:`gift-switch-track`,"aria-hidden":`true`,children:(0,V.jsx)(`span`,{})}),(0,V.jsx)(`span`,{children:e(`gifts.enableAfterImport`)})]}),I&&(0,V.jsx)(Ke,{children:I}),O&&(0,V.jsxs)(`div`,{className:`gift-validation`,children:[(0,V.jsxs)(`div`,{className:`gift-validation-head`,children:[(0,V.jsx)(k,{size:17}),(0,V.jsxs)(`div`,{children:[(0,V.jsx)(`strong`,{children:e(`gifts.validationReady`)}),(0,V.jsx)(`span`,{children:e(`gifts.validationHint`)})]})]}),(0,V.jsx)(`pre`,{children:JSON.stringify(O.details,null,2)})]})]}),(0,V.jsxs)(`div`,{className:`modal-actions`,children:[(0,V.jsx)(`button`,{className:`btn`,type:`button`,onClick:()=>o(!1),disabled:M,children:e(`common.close`)}),(0,V.jsxs)(`button`,{className:`btn`,type:`button`,onClick:ie,disabled:M,children:[M?(0,V.jsx)(A,{className:`spin`,size:15}):(0,V.jsx)(pe,{size:15}),e(`gifts.validate`)]}),(0,V.jsxs)(`button`,{className:`btn primary`,type:`button`,onClick:ae,disabled:M||!O,children:[(0,V.jsx)(_e,{size:15}),e(`gifts.confirmImport`)]})]})]})}),document.body),s&&(0,V.jsx)(Tt,{gift:s,onClose:()=>c(null),onPublished:()=>void ee()})]})}function kt({route:e,navigate:t}){let n=e.path.match(/^\/accounts\/(\d+)$/)?.[1],r=e.path.match(/^\/channels\/(\d+)$/)?.[1];return n?(0,V.jsx)(tt,{id:Number(n),navigate:t}):r?(0,V.jsx)(ot,{id:Number(r),navigate:t}):e.path===`/accounts`?(0,V.jsx)(at,{navigate:t}):e.path===`/channels`?(0,V.jsx)(st,{navigate:t}):e.path===`/gifts`?(0,V.jsx)(Ot,{}):e.path===`/messages/detail`||e.path===`/messages/private/detail`?(0,V.jsx)(mt,{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,V.jsx)(ut,{channelID:Number(e.search.get(`channel_id`)||`0`),msgID:Number(e.search.get(`msg_id`)||`0`),navigate:t}):e.path===`/messages/groups`?(0,V.jsx)(pt,{navigate:t}):e.path===`/messages`||e.path===`/messages/private`?(0,V.jsx)(ht,{navigate:t}):(0,V.jsx)(ct,{navigate:t})}function At(){let[e,t]=(0,g.useState)(void 0),[n,r]=(0,g.useState)(()=>Oe());(0,g.useEffect)(()=>{let e=()=>r(Oe());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(Oe())};return e===void 0?(0,V.jsx)(Me,{}):e===null?(0,V.jsx)(Ze,{onLogin:t}):(0,V.jsx)(Ne,{actor:e,route:n,navigate:i,onLogout:()=>t(null),children:(0,V.jsx)(kt,{route:n,navigate:i})})}_.createRoot(document.getElementById(`root`)).render((0,V.jsx)(g.StrictMode,{children:(0,V.jsx)(Ce,{children:(0,V.jsx)(At,{})})})); \ No newline at end of file diff --git a/cmd/telesrv-admin/web/dist/assets/index-W1-UPxwc.css b/cmd/telesrv-admin/web/dist/assets/index-W1-UPxwc.css deleted file mode 100644 index 1d71b83b..00000000 --- a/cmd/telesrv-admin/web/dist/assets/index-W1-UPxwc.css +++ /dev/null @@ -1 +0,0 @@ -:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:#f3f5f7;--panel:#fff;--panel-subtle:#f8fafb;--panel-strong:#eef2f5;--line:#d9e1e8;--line-strong:#c2ccd6;--text:#101828;--muted:#667085;--muted-2:#98a2b3;--brand:#176d61;--brand-2:#245b9d;--good:#167447;--warn:#a15c07;--danger:#b42318;--sidebar:#11161d;--sidebar-soft:#1b222b;--sidebar-line:#2c3541;--focus:#176d6129;--shadow:0 18px 52px #10182824}*{box-sizing:border-box}html,body,#root{min-height:100%}body{color:var(--text);background:var(--bg);margin:0;font:13px/1.45 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}button,input,textarea{font:inherit}a{color:inherit;text-decoration:none}.shell{grid-template-columns:232px minmax(0,1fr);min-height:100vh;display:grid}.sidebar{color:#eef2f6;background:var(--sidebar);border-right:1px solid var(--sidebar-line);flex-direction:column;gap:16px;height:100vh;padding:18px 12px;display:flex;position:sticky;top:0}.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:0 8px 24px #176d6142}.brand-mark{color:#fff;background:var(--brand);border:1px solid #fff3;border-radius:8px;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:#aeb8c4;margin-top:3px;font-size:11px;display:block}.sidebar-label{color:#8492a6;text-transform:uppercase;padding:0 8px;font-size:11px;font-weight:700}.nav-list,.nav-section{gap:4px;display:grid}.nav-section-toggle{color:#8fa0b4;cursor:pointer;text-align:left;background:0 0;border:1px solid #0000;border-radius:7px;grid-template-columns:18px minmax(0,1fr) 16px;align-items:center;gap:9px;width:100%;min-height:38px;padding:0 10px;font-size:12px;font-weight:800;display:grid}.nav-section-toggle:hover,.nav-section.active .nav-section-toggle{color:#fff;background:var(--sidebar-soft);border-color:#34404d}.nav-section-chevron{color:#8fa0b4;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{color:#c6d0dc;border:1px solid #0000;border-radius:7px;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:9px;min-height:38px;padding:0 10px;display:grid}.nav-dot{background:#687789;border-radius:999px;justify-self:center;width:6px;height:6px}.nav-item:hover,.nav-item.active{color:#fff;background:var(--sidebar-soft);border-color:#34404d}.nav-item.active .nav-dot{background:var(--brand)}.sidebar-status{gap:7px;margin-top:auto;display:grid}.runtime-row{color:#cbd5df;background:#171d25;border:1px solid #27313c;border-radius:7px;grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:7px;min-height:32px;padding:0 8px;display:grid}.runtime-row strong{color:#fff;font-size:11px}.workspace{min-width:0}.topbar{z-index:20;border-bottom:1px solid var(--line);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);background:#fffffff0;justify-content:space-between;align-items:center;gap:18px;min-height:66px;padding:12px 24px;display:flex;position:sticky;top:0}.topbar h1{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}.language-switch button.active{color:#fff;background:var(--brand)}.language-switch button:focus-visible{outline:2px solid var(--brand);outline-offset:2px}.actor-pill{color:#344054;background:var(--panel-subtle);border:1px solid var(--line);border-radius:999px;align-items:center;min-height:30px;padding:0 10px;display:inline-flex}.content{gap:16px;padding:18px 24px 30px;display:grid}.eyebrow{color:var(--muted);text-transform:uppercase;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:8px;min-width:0}.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{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:7px;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:#afd8bf}.status-item.warn,.metric.warn{border-color:#e7c77e}.metric.danger{border-color:#efb4ad}.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:8px;grid-template-columns:38px minmax(0,1fr) 18px;align-items:center;gap:12px;min-height:94px;padding:14px;display:grid}.launcher:hover{border-color:var(--brand)}.launcher-icon{width:38px;height:38px;color:var(--brand);background:#edf7f4;border:1px solid #c9e2dc;border-radius:8px;place-items:center;display:grid}.launcher-copy{gap:4px;display:grid}.launcher-copy strong{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{color:#344054;background:var(--panel);border:1px solid var(--line);border-radius:8px;align-items:center;gap:8px;min-height:38px;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:8px;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{border:1px solid var(--line);background:#fff;border-radius:8px;gap:8px;min-width:0;padding:10px;display:grid}.picker-head{color:#344054;justify-content:space-between;align-items:center;gap:8px;min-height:24px;font-weight:800;display:flex}.selected-entity{color:#0f3f38;background:#eef8f5;border:1px solid #b9dcd3;border-radius:7px;grid-template-columns:18px minmax(0,1fr) auto;align-items:center;gap:8px;min-height:40px;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:#52606d;font-size:11px}.picker-search{background:var(--panel-subtle);border:1px solid var(--line-strong);border-radius:7px;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:7px;max-height:236px;display:grid;overflow:auto}.picker-row{min-height:36px;color:var(--text);border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;background:#fff;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:#f3f8f6}.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:#fff2f0;border:1px solid #efb4ad;border-radius:7px}input,textarea{color:var(--text);border:1px solid var(--line-strong);background:#fff;border-radius:7px;outline:none}input{width:190px;height:34px;padding:0 10px}textarea{resize:vertical;width:100%;padding:9px 10px}input: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{border:1px solid var(--line-strong);background:#fff;border-radius:7px;align-items:center;gap:8px;width:min(380px,100%);height:34px;padding:0 10px;display:inline-flex}.searchbox input{width:100%;height:30px;box-shadow:none;border:0;padding:0}.btn{color:#1d2939;border:1px solid var(--line-strong);cursor:pointer;white-space:nowrap;background:#fff;border-radius:7px;justify-content:center;align-items:center;gap:6px;min-height:34px;padding:0 12px;display:inline-flex}.btn:hover:not(:disabled){background:#f7f9fb}.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:#12594f}.btn.ghost{background:var(--panel-subtle)}.btn.danger{color:var(--danger);background:#fff7f5;border-color:#efb4ad}.btn.danger:hover:not(:disabled){background:#ffeceb}.btn.warn{color:var(--warn);background:#fff8ec;border-color:#e7c77e}.btn.warn:hover:not(:disabled){background:#fff1d6}.btn:disabled,.btn.primary:disabled,.btn.warn:disabled,.btn.danger:disabled{color:var(--muted-2);border-color:var(--line);cursor:not-allowed;background:#f3f5f7}.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{border:1px solid var(--line);border-radius:8px;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:#475467;background:var(--panel-strong);font-weight:800;position:sticky;top:0}.data-table tbody tr:hover{background:#fbfcfd}.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{color:#4f5b68;white-space:nowrap;background:#f3f6f8;border:1px solid #d7e0e8;border-radius:999px;align-items:center;min-height:22px;padding:1px 8px;display:inline-flex}.badge.good{color:var(--good);background:#eef8f2;border-color:#b9dcc7}.badge.danger{color:var(--danger);background:#fff2f0;border-color:#efb4ad}.badge.warn{color:var(--warn);background:#fff8e7;border-color:#e7c77e}.empty-cell{color:var(--muted);text-align:center}.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:8px;justify-content:space-between;align-items:flex-start;gap:14px;padding:14px;display:flex}.entity-title{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:#344054;border:1px solid var(--line);background:#fbfcfd;border-radius:8px;margin:0;padding:10px}.section-block,.action-dock,.surface{background:var(--panel);border:1px solid var(--line);border-radius:8px;min-width:0;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:#344054;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:8px;flex-wrap:wrap;align-items:center;gap:8px;padding:10px;display:flex}.operation-title{align-items:center;gap:6px;width:100%;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:#8a251d;background:#fff2f0;border:1px solid #efb4ad;border-radius:8px;align-items:flex-start;gap:8px;padding:9px 10px;display:flex}.json-block{color:#d8e6f0;background:#141a22;border:1px solid #2a3542;border-radius:8px;max-height:520px;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:8px;place-items:center;display:grid}.modal-backdrop{z-index:10000;background:#11182785;place-items:center;padding:24px;display:grid;position:fixed;inset:0}.modal{border:1px solid var(--line);width:min(760px,100%);max-height:min(820px,100vh - 48px);box-shadow:var(--shadow);background:#fff;border-radius:8px;padding:0;overflow:hidden}.command-modal{flex-direction:column;display:flex}.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{background:var(--panel-subtle);border:1px solid var(--line);cursor:pointer;border-radius:7px;place-items:center;width:30px;height:30px;display:grid}.command-steps{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;display:grid}.command-body{gap:12px;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:8px;align-items:center;gap:8px;padding:0 10px;display:flex}.command-step span{border:1px solid var(--line);background:#fff;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:#a9d8ce}.command-step.done{color:var(--good);border-color:#b9dcc7}.form-field{gap:6px;display:grid}.form-field span,.form-stack span{color:#4b5563;font-weight:800}.command-preview{gap:8px;display:grid}.command-preview .json-block{max-height:150px}.preview-head,.result-title{color:#344054;align-items:center;gap:7px;font-weight:800;display:flex}.result-box{border:1px solid var(--line);background:#fbfcfd;border-radius:8px;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:#344054}.modal-actions{border-top:1px solid var(--line);background:#fff;justify-content:flex-end;padding:12px 18px}.login-page{background:var(--bg);place-items:center;min-height:100vh;padding:24px;display:grid}.login-panel{border:1px solid var(--line);width:min(420px,100%);box-shadow:var(--shadow);background:#fff;border-radius:8px;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:#edf7f4;border:1px solid #c9e2dc;border-radius:999px;align-items:center;padding:0 8px;font-size:12px;display:inline-flex}.login-copy h1{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{align-content:center;place-items:center;gap:18px;min-height:100vh;display:grid}.loader-bar{background:#d7dde4;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 c0fd45ce..7fd58c5c 100644 --- a/cmd/telesrv-admin/web/dist/index.html +++ b/cmd/telesrv-admin/web/dist/index.html @@ -4,8 +4,8 @@ telesrv admin - - + +
diff --git a/cmd/telesrv-admin/web/package-lock.json b/cmd/telesrv-admin/web/package-lock.json index c905b1f3..52c8bdd2 100644 --- a/cmd/telesrv-admin/web/package-lock.json +++ b/cmd/telesrv-admin/web/package-lock.json @@ -8,6 +8,7 @@ "name": "telesrv-admin-ui", "version": "0.1.0", "dependencies": { + "lottie-web": "^5.13.0", "lucide-react": "^0.468.0", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -741,6 +742,12 @@ "loose-envify": "cli.js" } }, + "node_modules/lottie-web": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.13.0.tgz", + "integrity": "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==", + "license": "MIT" + }, "node_modules/lucide-react": { "version": "0.468.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", diff --git a/cmd/telesrv-admin/web/package.json b/cmd/telesrv-admin/web/package.json index 88828dd8..f215498c 100644 --- a/cmd/telesrv-admin/web/package.json +++ b/cmd/telesrv-admin/web/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "lottie-web": "^5.13.0", "lucide-react": "^0.468.0", "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/cmd/telesrv-admin/web/src/api.ts b/cmd/telesrv-admin/web/src/api.ts index 61591052..dd4a3245 100644 --- a/cmd/telesrv-admin/web/src/api.ts +++ b/cmd/telesrv-admin/web/src/api.ts @@ -7,7 +7,9 @@ import type { GroupMessageDetail, GroupMessageListResponse, MessageDetail, - MessageListResponse + MessageListResponse, + StarGiftCollectiblePreview, + StarGiftListResponse } from "./types"; export class APIError extends Error { @@ -20,12 +22,10 @@ export class APIError extends Error { } async function request(url: string, init: RequestInit = {}): Promise { + const isForm = typeof FormData !== "undefined" && init.body instanceof FormData; const response = await fetch(url, { credentials: "same-origin", - headers: { - "Content-Type": "application/json", - ...(init.headers ?? {}) - }, + headers: isForm ? init.headers : { "Content-Type": "application/json", ...(init.headers ?? {}) }, ...init }); const text = await response.text(); @@ -65,6 +65,12 @@ export const api = { const params = new URLSearchParams({ channel_id: String(channelID), msg_id: String(msgID) }); return request(`/api/messages/groups/detail?${params.toString()}`); }, + gifts: () => request("/api/gifts"), + giftAnimation: (id: number) => request>(`/api/gifts/${id}/animation`), + giftCollectibles: (id: number) => request(`/api/gifts/${id}/collectibles`), + giftCollectibleAnimation: (giftID: number, kind: "model" | "pattern", attributeID: number) => request>(`/api/gifts/${giftID}/collectibles/${kind}/${attributeID}/animation`), + importGift: (form: FormData) => request("/api/actions/import-gift", { method: "POST", body: form }), + publishGiftCollectibles: (giftID: number, form: FormData) => request(`/api/actions/publish-gift-collectibles?gift_id=${giftID}`, { method: "POST", body: form }), action: (path: string, payload: Record) => request(path, { method: "POST", body: JSON.stringify(payload) diff --git a/cmd/telesrv-admin/web/src/components/Layout.tsx b/cmd/telesrv-admin/web/src/components/Layout.tsx index 2f226127..41c81c64 100644 --- a/cmd/telesrv-admin/web/src/components/Layout.tsx +++ b/cmd/telesrv-admin/web/src/components/Layout.tsx @@ -7,7 +7,8 @@ import { Server, Shield, ShieldCheck, - Users + Users, + Gift } from "lucide-react"; import { useEffect, useState, type ReactNode } from "react"; import { api } from "../api"; @@ -74,6 +75,7 @@ export function Shell({ } href="/" route={route} navigate={navigate}>{t("layout.dashboard")} } href="/accounts" route={route} navigate={navigate}>{t("layout.accounts")} } href="/channels" route={route} navigate={navigate}>{t("layout.channels")} + } href="/gifts" route={route} navigate={navigate}>{t("layout.gifts")}
+ +
+ {rows.map((row, index) =>
+
{index + 1}
+ + + + +
{row.animation ? : }
+ + {row.fileError && {row.fileError}} +
)} +
+ + ); + + return createPortal(
+
+
+
{t("collectibles.eyebrow")}

{t("collectibles.title", { id: gift.GiftID })}

{gift.Title || `Gift #${gift.GiftID}`}

+ +
+
+ {loading ?
{t("common.loading")}
: active?.found ?
+
{t("collectibles.activeRevision", { revision: active.revision ?? 0 })}{active.slug_prefix} · ⭐ {active.upgrade_stars} · {active.issued} / {active.supply_total}
{t("collectibles.published")}
+
+ {[...(active.models ?? []), ...(active.patterns ?? [])].map((attribute) =>
{attribute.name}{t(`collectibles.${attribute.kind}`)} · {attribute.rarity_permille}‰
)} + {(active.backdrops ?? []).map((attribute) =>
Aa
{attribute.name}{t("collectibles.backdrop")} · {attribute.rarity_permille}‰
)} +
+
:
{t("collectibles.noPool")}{t("collectibles.noPoolHint")}
} + +
+
{t("collectibles.publishNew")}{t("collectibles.immutableHint")}
TGSLottie JSON
+
+ + + + +
+ {renderAnimatedRows("models", models, setModels)} + {renderAnimatedRows("patterns", patterns, setPatterns)} +
+
{t("collectibles.backdrops")}{t("collectibles.colorHint")}
{rarityTotals.backdrops} / 1000
+
{backdrops.map((row, index) =>
+
{index + 1}
+ + + + + {(["center", "edge", "pattern", "text"] as const).map((field) => )} +
Aa
+ +
)}
+
+
+ {error && {error}} + {preview &&
{t("collectibles.validationReady")}{t("collectibles.validationHint")}
{JSON.stringify(preview.details, null, 2)}
} +
+
+ + + +
+
+
, document.body); +} diff --git a/cmd/telesrv-admin/web/src/pages/GiftsPage.tsx b/cmd/telesrv-admin/web/src/pages/GiftsPage.tsx new file mode 100644 index 00000000..79e2b53e --- /dev/null +++ b/cmd/telesrv-admin/web/src/pages/GiftsPage.tsx @@ -0,0 +1,237 @@ +import { CheckCircle2, FileJson2, Gem, Loader2, Pause, Play, Plus, RefreshCw, Search, ShieldCheck, Upload, X } from "lucide-react"; +import lottie from "lottie-web/build/player/lottie_light_canvas"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { api, errorMessage } from "../api"; +import { ActionButton } from "../components/ActionButton"; +import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui"; +import { useI18n } from "../i18n"; +import { formatDate } from "../lib/format"; +import type { CommandResult, StarGiftRow } from "../types"; +import { GiftCollectiblesModal } from "./GiftCollectiblesModal"; + +function formatBytes(bytes: number) { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + +function LottiePreview({ giftID, revision, compact = false }: { giftID: number; revision: number; compact?: boolean }) { + const host = useRef(null); + const animation = useRef | null>(null); + const [playing, setPlaying] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + let cancelled = false; + api.giftAnimation(giftID).then((data) => { + if (cancelled || !host.current) return; + animation.current?.destroy(); + animation.current = lottie.loadAnimation({ + container: host.current, + renderer: "canvas", + loop: true, + autoplay: true, + animationData: structuredClone(data) + }); + }).catch((err) => setError(errorMessage(err))); + return () => { + cancelled = true; + animation.current?.destroy(); + animation.current = null; + }; + }, [giftID, revision]); + + function toggle() { + if (!animation.current) return; + if (playing) animation.current.pause(); + else animation.current.play(); + setPlaying(!playing); + } + + return ( +
+
{error && {error}}
+ +
+ ); +} + +export function GiftsPage() { + const { t } = useI18n(); + const [gifts, setGifts] = useState([]); + const [query, setQuery] = useState(""); + const [importOpen, setImportOpen] = useState(false); + const [collectibleGift, setCollectibleGift] = useState(null); + const [file, setFile] = useState(null); + const [giftID, setGiftID] = useState(0); + const [title, setTitle] = useState(""); + const [stars, setStars] = useState("50"); + const [convertStars, setConvertStars] = useState("50"); + const [sortOrder, setSortOrder] = useState("0"); + const [enabled, setEnabled] = useState(true); + const [reason, setReason] = useState(""); + const [preview, setPreview] = useState(null); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + const [importError, setImportError] = useState(""); + + async function load() { + setError(""); + try { + setGifts((await api.gifts()).Gifts ?? []); + } catch (err) { + setError(errorMessage(err)); + } + } + + useEffect(() => { void load(); }, []); + + const visibleGifts = useMemo(() => { + const normalized = query.trim().toLowerCase(); + if (!normalized) return gifts; + return gifts.filter((gift) => + String(gift.GiftID).includes(normalized) || + gift.Title.toLowerCase().includes(normalized) || + gift.SourceFormat.toLowerCase().includes(normalized) + ); + }, [gifts, query]); + + function uploadForm(confirm: boolean, commandID = "") { + if (!file) throw new Error(t("gifts.fileRequired")); + if (!reason.trim()) throw new Error(t("action.reasonRequired")); + const form = new FormData(); + form.set("metadata", JSON.stringify({ + command_id: commandID, + reason: reason.trim(), + confirm, + gift_id: giftID, + title: title.trim(), + stars: Number(stars), + convert_stars: Number(convertStars), + enabled, + sort_order: Number(sortOrder) + })); + form.set("file", file, file.name); + return form; + } + + async function validateImport() { + setBusy(true); setImportError(""); setPreview(null); + try { + setPreview(await api.importGift(uploadForm(false))); + } catch (err) { + setImportError(errorMessage(err)); + } finally { setBusy(false); } + } + + async function confirmImport() { + if (!preview) return; + setBusy(true); setImportError(""); + try { + await api.importGift(uploadForm(true, preview.command_id)); + setPreview(null); setFile(null); setGiftID(0); setTitle(""); + await load(); + setImportOpen(false); + } catch (err) { + setImportError(errorMessage(err)); + } finally { setBusy(false); } + } + + function startImport() { + setGiftID(0); setTitle(""); setStars("50"); setConvertStars("50"); setSortOrder("0"); + setEnabled(true); setReason(""); setFile(null); setPreview(null); setImportError(""); setImportOpen(true); + } + + function startRevision(gift: StarGiftRow) { + setGiftID(gift.GiftID); setTitle(gift.Title); setStars(String(gift.Stars)); + setConvertStars(String(gift.ConvertStars)); setSortOrder(String(gift.SortOrder)); setEnabled(gift.Enabled); + setReason(""); setFile(null); setPreview(null); setImportError(""); setImportOpen(true); + } + + return ( + + + + }> + {error && {error}} +
+ + gift.Enabled).length)} tone="good" /> + sum + gift.ReceivedCount, 0))} /> + +
+ +
+ + {t("gifts.listSummary", { shown: visibleGifts.length, total: gifts.length })} +
+
+
+ + + + {visibleGifts.map((gift) => ( + + + + + + + + + + + + ))} + {visibleGifts.length === 0 && } + +
{t("gifts.animation")}{t("gifts.idRevision")}{t("gifts.title")}{t("gifts.price")}{t("gifts.source")}{t("gifts.received")}{t("common.status")}{t("common.updatedAt")}{t("common.actions")}
{gift.GiftID} / {gift.Revision}{gift.Title || `Gift #${gift.GiftID}`}{t("gifts.sortOrder")}: {gift.SortOrder}⭐ {gift.Stars}→ {gift.ConvertStars}{gift.SourceFormat}{formatBytes(gift.AnimationSize)}{gift.ReceivedCount}{gift.Enabled ? t("common.enabled") : t("common.disabled")}{formatDate(gift.UpdatedAt)}
({ gift_id: gift.GiftID, enabled: !gift.Enabled })} onDone={() => void load()} />
+
+ + {importOpen && createPortal( +
+
+
+
{t("gifts.importEyebrow")}

{giftID ? t("gifts.newRevision", { id: giftID }) : t("gifts.importTitle")}

+ +
+
+
+
1{t("gifts.stepDetails")}
+
2{t("gifts.stepValidate")}
+
3{t("gifts.stepImport")}
+
+
{t("gifts.importHint")}
TGSLottie JSON
+ +
+ + + + +
+ + + {importError && {importError}} + {preview &&
{t("gifts.validationReady")}{t("gifts.validationHint")}
{JSON.stringify(preview.details, null, 2)}
} +
+
+ + + +
+
+
, + document.body + )} + {collectibleGift && setCollectibleGift(null)} onPublished={() => void load()} />} +
+ ); +} diff --git a/cmd/telesrv-admin/web/src/pages/Routes.tsx b/cmd/telesrv-admin/web/src/pages/Routes.tsx index 96ebac75..441a9e1d 100644 --- a/cmd/telesrv-admin/web/src/pages/Routes.tsx +++ b/cmd/telesrv-admin/web/src/pages/Routes.tsx @@ -8,6 +8,7 @@ import { GroupMessageDetailPage } from "./GroupMessageDetailPage"; import { GroupMessagesPage } from "./GroupMessagesPage"; import { MessageDetailPage } from "./MessageDetailPage"; import { MessagesPage } from "./MessagesPage"; +import { GiftsPage } from "./GiftsPage"; export function Routes({ route, navigate }: { route: RouteState; navigate: Navigate }) { const accountID = route.path.match(/^\/accounts\/(\d+)$/)?.[1]; @@ -24,6 +25,9 @@ export function Routes({ route, navigate }: { route: RouteState; navigate: Navig if (route.path === "/channels") { return ; } + if (route.path === "/gifts") { + return ; + } if (route.path === "/messages/detail" || route.path === "/messages/private/detail") { return ( div:first-child, +.collectible-section-head > div:first-child { display: grid; gap: 3px; } +.collectible-empty span, +.collectible-definition-head span, +.collectible-section-head span { color: var(--muted); font-size: 10px; font-weight: 500; } +.collectible-active { overflow: hidden; background: #ffffff; border: 1px solid #ddd6ee; border-radius: 12px; box-shadow: 0 5px 16px rgba(66, 46, 110, .05); } +.collectible-active-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 14px; background: linear-gradient(100deg, #fbf9ff, #f4f9ff); border-bottom: 1px solid #e9e4f3; } +.collectible-active-head > div { display: flex; align-items: center; gap: 9px; color: #60458f; } +.collectible-active-head > div > div { display: grid; gap: 2px; } +.collectible-active-head span { color: var(--muted); font-size: 10px; } +.collectible-active-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(145px, 1fr)); gap: 1px; background: var(--line); } +.collectible-active-grid article { display: flex; min-width: 0; align-items: center; gap: 9px; padding: 9px 11px; background: #ffffff; } +.collectible-active-grid article > div:last-child { display: grid; min-width: 0; gap: 2px; } +.collectible-active-grid article strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } +.collectible-active-grid article span { color: var(--muted); font-size: 9px; } +.collectible-definition { overflow: hidden; background: #ffffff; border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 8px 24px rgba(16, 24, 40, .04); } +.collectible-definition-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; background: linear-gradient(110deg, #f8fbfa, #fbf9ff); border-bottom: 1px solid var(--line); } +.collectible-main-fields { padding: 14px 16px; background: #fbfcfd; border-bottom: 1px solid var(--line); } +.collectible-section { padding: 14px 16px; border-bottom: 1px solid var(--line); } +.collectible-section:last-child { border-bottom: 0; } +.collectible-section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px; } +.collectible-section-tools { display: flex; align-items: center; gap: 7px; } +.collectible-rows { display: grid; gap: 7px; } +.collectible-row { position: relative; display: grid; align-items: end; gap: 7px; padding: 9px 9px 9px 36px; background: #fafbfc; border: 1px solid #e1e6eb; border-radius: 9px; } +.collectible-row:hover { background: #ffffff; border-color: #cbd7dd; box-shadow: 0 3px 10px rgba(16, 24, 40, .035); } +.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 { position: absolute; top: 0; bottom: 0; left: 0; display: grid; width: 27px; place-items: center; color: #71668c; background: #f0edf7; border-right: 1px solid #e0d9ed; border-radius: 8px 0 0 8px; font-size: 10px; font-weight: 800; } +.collectible-row label { display: grid; min-width: 0; gap: 4px; } +.collectible-row label > span { color: var(--muted); font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: .025em; } +.collectible-row input:not([type="file"]) { width: 100%; min-width: 0; height: 32px; padding: 0 8px; color: var(--text); background: #ffffff; border: 1px solid #d5dde3; border-radius: 7px; font: inherit; font-size: 11px; } +.collectible-row input:focus { border-color: #8d7aba; box-shadow: 0 0 0 3px rgba(111, 91, 174, .08); outline: none; } +.collectible-file input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; } +.collectible-file em { display: flex; min-width: 0; height: 32px; align-items: center; gap: 5px; overflow: hidden; padding: 0 8px; color: #625080; background: #f7f4fd; border: 1px dashed #cfc4e1; border-radius: 7px; font-size: 10px; font-style: normal; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; } +.collectible-inline-preview { display: grid; width: 42px; height: 42px; place-items: center; overflow: hidden; color: #8c7cae; background: radial-gradient(circle, #ffffff, #eee8f8); border: 1px solid #ded5ed; border-radius: 8px; } +.collectible-animation { width: 100%; height: 100%; overflow: hidden; } +.collectible-animation.compact { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; background: radial-gradient(circle, #ffffff, #f0ebfa); border: 1px solid #e0d9ec; border-radius: 8px; } +.collectible-animation canvas { width: 100% !important; height: 100% !important; } +.collectible-animation.failed { color: #b42318; background: #fff4f2; } +.collectible-animation.loading { color: #807397; } +.collectible-file-error { grid-column: 1 / -1; color: #b42318; font-size: 10px; } +.collectible-color input { height: 32px !important; padding: 3px !important; cursor: pointer; } +.collectible-backdrop-preview { display: grid; width: 42px; height: 42px; flex: 0 0 42px; place-items: center; border: 1px solid rgba(42, 31, 71, .18); border-radius: 8px; box-shadow: inset 0 0 0 1px rgba(255,255,255,.2); font-size: 11px; font-weight: 900; } +.collectible-row .icon-btn { align-self: center; } +.collectible-row .icon-btn:disabled { opacity: .28; } + +@media (max-width: 900px) { + .gift-fields-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .collectible-row.animated, + .collectible-row.backdrop { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .collectible-inline-preview, + .collectible-backdrop-preview, + .collectible-row .icon-btn { align-self: center; justify-self: start; } +} + +@media (max-width: 620px) { + .gift-import-note { align-items: flex-start; flex-direction: column; } + .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; } + .collectible-modal-body { padding: 10px; } + .collectible-definition-head, + .collectible-section-head { align-items: flex-start; flex-direction: column; } + .collectible-row.animated, + .collectible-row.backdrop { grid-template-columns: 1fr; } + .collectible-active-grid { grid-template-columns: 1fr 1fr; } +} diff --git a/cmd/telesrv-admin/web/src/types.ts b/cmd/telesrv-admin/web/src/types.ts index 990d6735..f95ca7fd 100644 --- a/cmd/telesrv-admin/web/src/types.ts +++ b/cmd/telesrv-admin/web/src/types.ts @@ -160,6 +160,58 @@ export type OutboxRow = { UpdatedAt: string; }; +export type StarGiftRow = { + GiftID: number; + RevisionID: number; + Revision: number; + Title: string; + Stars: number; + ConvertStars: number; + Enabled: boolean; + SortOrder: number; + DocumentID: number; + SourceName: string; + SourceFormat: "tgs" | "lottie"; + AnimationSHA: string; + AnimationSize: number; + Width: number; + Height: number; + FrameRate: number; + ReceivedCount: number; + CreatedBy: string; + UpdatedAt: string; +}; + +export type StarGiftListResponse = { Gifts: StarGiftRow[] }; + +export type StarGiftCollectibleAttributeRow = { + id: number; + kind: "model" | "pattern" | "backdrop"; + name: string; + rarity_permille: number; + sort_order: number; + source_name?: string; + source_format?: "tgs" | "lottie"; + backdrop_id?: number; + center_color?: number; + edge_color?: number; + pattern_color?: number; + text_color?: number; +}; + +export type StarGiftCollectiblePreview = { + found: boolean; + gift_id: number; + revision?: number; + upgrade_stars?: number; + supply_total?: number; + issued?: number; + slug_prefix?: string; + models?: StarGiftCollectibleAttributeRow[]; + patterns?: StarGiftCollectibleAttributeRow[]; + backdrops?: StarGiftCollectibleAttributeRow[]; +}; + export type MessageDetail = { Message: MessageRow; MessageJSON: string; diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index fc6b64d7..12e56e68 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -634,7 +634,9 @@ func run(logger *zap.Logger) error { starsStore := postgres.NewStarsStore(pool) starsService := stars.NewService(starsStore, stars.WithStartingGrant(cfg.StarsStartingGrant)) starGiftStore := postgres.NewStarGiftStore(pool) - giftsService := stargifts.NewService(starGiftStore, filesService) + starGiftUpgradeStore := postgres.NewStarGiftUpgradeStore(pool, messageStore) + giftsService := stargifts.NewService(starGiftStore, blobBackend, cfg.DC, + stargifts.WithUpgradeStore(starGiftUpgradeStore)) // Passkey:凭据持久化走 postgres;一次性挑战走进程内内存(短 TTL,与 QR 登录 token // 同属进程内一次性凭据,不跨实例)。 passkeyStore := postgres.NewPasskeyStore(pool) @@ -776,6 +778,7 @@ func run(logger *zap.Logger) error { RPCProjections: router, BaseUsers: userCache, BotProfiles: botsService, + StarGifts: giftsService, }, logger.Named("store").Named("read-model-listener")) go readModelListener.Run(ctx) activeSessions.SetLifecycleObserver(router) @@ -789,6 +792,7 @@ func run(logger *zap.Logger) error { Channels: channelsService, ChannelNotifier: router, Messages: messagesService, + Gifts: giftsService, }) // bot session 撤销、在线通知与 @ChatBot 流式草稿推送经 router 实现(需 tg.* 边界), // router 创建后注入。 diff --git a/deploy/migrations/0089_star_gift_catalog.down.sql b/deploy/migrations/0089_star_gift_catalog.down.sql new file mode 100644 index 00000000..b81c5fe0 --- /dev/null +++ b/deploy/migrations/0089_star_gift_catalog.down.sql @@ -0,0 +1,17 @@ +DROP TRIGGER IF EXISTS star_gift_catalog_changed ON public.star_gift_catalog; +DROP FUNCTION IF EXISTS public.telesrv_notify_star_gift_catalog_changed(); + +ALTER TABLE public.peer_star_gifts + DROP CONSTRAINT IF EXISTS peer_star_gifts_catalog_revision_fk, + DROP COLUMN IF EXISTS catalog_revision_id; +DROP INDEX IF EXISTS public.peer_star_gifts_gift_idx; + +ALTER TABLE public.star_gift_catalog + DROP CONSTRAINT IF EXISTS star_gift_catalog_active_revision_fk; +DROP TABLE IF EXISTS public.star_gift_catalog_revisions; +DROP TABLE IF EXISTS public.star_gift_catalog; +DROP SEQUENCE IF EXISTS public.star_gift_catalog_revision_id_seq; +DROP SEQUENCE IF EXISTS public.star_gift_catalog_gift_id_seq; + +DELETE FROM public.read_model_versions +WHERE model = 'star_gift_catalog' AND owner_user_id = 0 AND peer_type = '' AND peer_id = 0; diff --git a/deploy/migrations/0089_star_gift_catalog.up.sql b/deploy/migrations/0089_star_gift_catalog.up.sql new file mode 100644 index 00000000..ab466e4d --- /dev/null +++ b/deploy/migrations/0089_star_gift_catalog.up.sql @@ -0,0 +1,81 @@ +-- Durable, administrator-managed regular Star Gift catalog. The previous seven-item +-- animated_emoji-derived directory was development-only and is intentionally not migrated. +-- Existing received rows refer to those non-durable definitions, so clear them instead of +-- manufacturing a read-time compatibility fallback that could no longer reconstruct assets. + +CREATE SEQUENCE public.star_gift_catalog_gift_id_seq AS bigint START WITH 9000000000000001; +CREATE SEQUENCE public.star_gift_catalog_revision_id_seq AS bigint START WITH 1; + +CREATE TABLE public.star_gift_catalog ( + gift_id bigint DEFAULT nextval('public.star_gift_catalog_gift_id_seq') NOT NULL, + active_revision_id bigint NOT NULL, + enabled boolean DEFAULT true NOT NULL, + sort_order integer DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT star_gift_catalog_pkey PRIMARY KEY (gift_id) +); + +CREATE TABLE public.star_gift_catalog_revisions ( + id bigint DEFAULT nextval('public.star_gift_catalog_revision_id_seq') NOT NULL, + gift_id bigint NOT NULL, + revision integer NOT NULL, + title text DEFAULT '' NOT NULL, + stars bigint NOT NULL, + convert_stars bigint NOT NULL, + document_id bigint NOT NULL, + animation_json jsonb NOT NULL, + animation_sha256 bytea NOT NULL, + source_name text DEFAULT '' NOT NULL, + source_format text NOT NULL, + width integer NOT NULL, + height integer NOT NULL, + frame_rate double precision DEFAULT 0 NOT NULL, + in_point double precision DEFAULT 0 NOT NULL, + out_point double precision DEFAULT 0 NOT NULL, + created_by text DEFAULT '' NOT NULL, + command_id text DEFAULT '' NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT star_gift_catalog_revisions_pkey PRIMARY KEY (id), + CONSTRAINT star_gift_catalog_revisions_gift_revision_uniq UNIQUE (gift_id, revision), + CONSTRAINT star_gift_catalog_revisions_document_uniq UNIQUE (document_id), + CONSTRAINT star_gift_catalog_revision_price_check CHECK (stars > 0 AND convert_stars >= 0 AND convert_stars <= stars), + CONSTRAINT star_gift_catalog_revision_shape_check CHECK (width = 512 AND height = 512 AND jsonb_typeof(animation_json) = 'object'), + CONSTRAINT star_gift_catalog_revision_source_check CHECK (source_format IN ('tgs', 'lottie')), + CONSTRAINT star_gift_catalog_revision_gift_fk FOREIGN KEY (gift_id) + REFERENCES public.star_gift_catalog(gift_id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT star_gift_catalog_revision_document_fk FOREIGN KEY (document_id) + REFERENCES public.documents(id) ON DELETE RESTRICT +); + +ALTER TABLE public.star_gift_catalog + ADD CONSTRAINT star_gift_catalog_active_revision_fk FOREIGN KEY (active_revision_id) + REFERENCES public.star_gift_catalog_revisions(id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED; + +CREATE INDEX star_gift_catalog_enabled_order_idx + ON public.star_gift_catalog (sort_order, gift_id) WHERE enabled; +CREATE INDEX star_gift_catalog_revisions_gift_idx + ON public.star_gift_catalog_revisions (gift_id, revision DESC); + +DELETE FROM public.peer_star_gifts; +ALTER TABLE public.peer_star_gifts + ADD COLUMN catalog_revision_id bigint NOT NULL, + ADD CONSTRAINT peer_star_gifts_catalog_revision_fk FOREIGN KEY (catalog_revision_id) + REFERENCES public.star_gift_catalog_revisions(id) ON DELETE RESTRICT; +CREATE INDEX peer_star_gifts_catalog_revision_idx + ON public.peer_star_gifts (catalog_revision_id); +CREATE INDEX peer_star_gifts_gift_idx + ON public.peer_star_gifts (gift_id); + +CREATE FUNCTION public.telesrv_notify_star_gift_catalog_changed() RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + PERFORM public.telesrv_bump_read_model_version('star_gift_catalog', 0, '', 0); + RETURN NULL; +END; +$$; + +CREATE TRIGGER star_gift_catalog_changed + AFTER INSERT OR UPDATE OR DELETE ON public.star_gift_catalog + FOR EACH STATEMENT EXECUTE FUNCTION public.telesrv_notify_star_gift_catalog_changed(); diff --git a/deploy/migrations/0090_star_gift_collectibles.down.sql b/deploy/migrations/0090_star_gift_collectibles.down.sql new file mode 100644 index 00000000..19970a8f --- /dev/null +++ b/deploy/migrations/0090_star_gift_collectibles.down.sql @@ -0,0 +1,31 @@ +DROP TRIGGER IF EXISTS star_gift_collectible_backdrop_guard ON public.star_gift_collectible_backdrops; +DROP TRIGGER IF EXISTS star_gift_collectible_pattern_guard ON public.star_gift_collectible_patterns; +DROP TRIGGER IF EXISTS star_gift_collectible_model_guard ON public.star_gift_collectible_models; +DROP TRIGGER IF EXISTS star_gift_collectible_revision_guard ON public.star_gift_collectible_revisions; +DROP FUNCTION IF EXISTS public.telesrv_guard_collectible_attribute(); +DROP FUNCTION IF EXISTS public.telesrv_guard_collectible_revision(); + +DROP TABLE IF EXISTS public.star_gift_collection_items; +DROP TABLE IF EXISTS public.star_gift_collections; +DROP TABLE IF EXISTS public.star_gift_upgrade_commands; + +DROP INDEX IF EXISTS public.peer_star_gifts_unique_gift_uniq; +ALTER TABLE public.peer_star_gifts + DROP CONSTRAINT IF EXISTS peer_star_gifts_pinned_order_check, + DROP CONSTRAINT IF EXISTS peer_star_gifts_terminal_state_check, + DROP CONSTRAINT IF EXISTS peer_star_gifts_unique_gift_fk, + DROP COLUMN IF EXISTS pinned_order, + DROP COLUMN IF EXISTS upgrade_msg_id, + DROP COLUMN IF EXISTS unique_gift_id; + +DROP TABLE IF EXISTS public.unique_star_gifts; + +ALTER TABLE public.star_gift_catalog + DROP CONSTRAINT IF EXISTS star_gift_catalog_collectible_revision_fk, + DROP COLUMN IF EXISTS collectible_revision_id; + +DROP TABLE IF EXISTS public.star_gift_collectible_backdrops; +DROP TABLE IF EXISTS public.star_gift_collectible_patterns; +DROP TABLE IF EXISTS public.star_gift_collectible_models; +DROP TABLE IF EXISTS public.star_gift_collectible_revisions; +DROP SEQUENCE IF EXISTS public.unique_star_gift_id_seq; diff --git a/deploy/migrations/0090_star_gift_collectibles.up.sql b/deploy/migrations/0090_star_gift_collectibles.up.sql new file mode 100644 index 00000000..fd50c5cd --- /dev/null +++ b/deploy/migrations/0090_star_gift_collectibles.up.sql @@ -0,0 +1,238 @@ +-- Collectible Star Gifts: immutable published attribute pools, unique gift instances and +-- peer-owned gift collections. Marketplace/transfer/auction/craft/TON state is intentionally +-- absent from this schema. + +CREATE SEQUENCE public.unique_star_gift_id_seq AS bigint START WITH 9200000000000001; + +CREATE TABLE public.star_gift_collectible_revisions ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + gift_id bigint NOT NULL REFERENCES public.star_gift_catalog(gift_id) ON DELETE RESTRICT, + revision integer NOT NULL, + upgrade_stars bigint NOT NULL, + supply_total integer NOT NULL, + issued integer DEFAULT 0 NOT NULL, + slug_prefix text NOT NULL, + status text DEFAULT 'draft' NOT NULL, + created_by text DEFAULT '' NOT NULL, + command_id text DEFAULT '' NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + published_at timestamp with time zone, + CONSTRAINT star_gift_collectible_revision_uniq UNIQUE (gift_id, revision), + CONSTRAINT star_gift_collectible_command_uniq UNIQUE (gift_id, command_id), + CONSTRAINT star_gift_collectible_price_check CHECK (upgrade_stars > 0), + CONSTRAINT star_gift_collectible_supply_check CHECK (supply_total > 0 AND issued >= 0 AND issued <= supply_total), + CONSTRAINT star_gift_collectible_slug_check CHECK (slug_prefix ~ '^[a-z0-9][a-z0-9-]{0,47}$'), + CONSTRAINT star_gift_collectible_status_check CHECK (status IN ('draft', 'published')), + CONSTRAINT star_gift_collectible_publish_check CHECK ( + (status = 'draft' AND published_at IS NULL) OR + (status = 'published' AND published_at IS NOT NULL) + ) +); + +CREATE TABLE public.star_gift_collectible_models ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + collectible_revision_id bigint NOT NULL REFERENCES public.star_gift_collectible_revisions(id) ON DELETE CASCADE, + name text NOT NULL, + document_id bigint NOT NULL REFERENCES public.documents(id) ON DELETE RESTRICT, + animation_json jsonb NOT NULL, + animation_sha256 bytea NOT NULL, + source_name text DEFAULT '' NOT NULL, + source_format text NOT NULL, + width integer NOT NULL, + height integer NOT NULL, + frame_rate double precision DEFAULT 0 NOT NULL, + in_point double precision DEFAULT 0 NOT NULL, + out_point double precision DEFAULT 0 NOT NULL, + rarity_permille integer NOT NULL, + sort_order integer DEFAULT 0 NOT NULL, + CONSTRAINT star_gift_collectible_model_name_uniq UNIQUE (collectible_revision_id, name), + CONSTRAINT star_gift_collectible_model_id_revision_uniq UNIQUE (id, collectible_revision_id), + CONSTRAINT star_gift_collectible_model_rarity_check CHECK (rarity_permille BETWEEN 1 AND 1000), + CONSTRAINT star_gift_collectible_model_shape_check CHECK (width = 512 AND height = 512 AND jsonb_typeof(animation_json) = 'object'), + CONSTRAINT star_gift_collectible_model_source_check CHECK (source_format IN ('tgs', 'lottie')) +); + +CREATE TABLE public.star_gift_collectible_patterns ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + collectible_revision_id bigint NOT NULL REFERENCES public.star_gift_collectible_revisions(id) ON DELETE CASCADE, + name text NOT NULL, + document_id bigint NOT NULL REFERENCES public.documents(id) ON DELETE RESTRICT, + animation_json jsonb NOT NULL, + animation_sha256 bytea NOT NULL, + source_name text DEFAULT '' NOT NULL, + source_format text NOT NULL, + width integer NOT NULL, + height integer NOT NULL, + frame_rate double precision DEFAULT 0 NOT NULL, + in_point double precision DEFAULT 0 NOT NULL, + out_point double precision DEFAULT 0 NOT NULL, + rarity_permille integer NOT NULL, + sort_order integer DEFAULT 0 NOT NULL, + CONSTRAINT star_gift_collectible_pattern_name_uniq UNIQUE (collectible_revision_id, name), + CONSTRAINT star_gift_collectible_pattern_id_revision_uniq UNIQUE (id, collectible_revision_id), + CONSTRAINT star_gift_collectible_pattern_rarity_check CHECK (rarity_permille BETWEEN 1 AND 1000), + CONSTRAINT star_gift_collectible_pattern_shape_check CHECK (width = 512 AND height = 512 AND jsonb_typeof(animation_json) = 'object'), + CONSTRAINT star_gift_collectible_pattern_source_check CHECK (source_format IN ('tgs', 'lottie')) +); + +CREATE TABLE public.star_gift_collectible_backdrops ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + collectible_revision_id bigint NOT NULL REFERENCES public.star_gift_collectible_revisions(id) ON DELETE CASCADE, + name text NOT NULL, + backdrop_id integer NOT NULL, + center_color integer NOT NULL, + edge_color integer NOT NULL, + pattern_color integer NOT NULL, + text_color integer NOT NULL, + rarity_permille integer NOT NULL, + sort_order integer DEFAULT 0 NOT NULL, + CONSTRAINT star_gift_collectible_backdrop_name_uniq UNIQUE (collectible_revision_id, name), + CONSTRAINT star_gift_collectible_backdrop_display_uniq UNIQUE (collectible_revision_id, backdrop_id), + CONSTRAINT star_gift_collectible_backdrop_id_revision_uniq UNIQUE (id, collectible_revision_id), + CONSTRAINT star_gift_collectible_backdrop_rarity_check CHECK (rarity_permille BETWEEN 1 AND 1000), + CONSTRAINT star_gift_collectible_backdrop_color_check CHECK ( + center_color BETWEEN 0 AND 16777215 AND edge_color BETWEEN 0 AND 16777215 AND + pattern_color BETWEEN 0 AND 16777215 AND text_color BETWEEN 0 AND 16777215 + ) +); + +ALTER TABLE public.star_gift_catalog + ADD COLUMN collectible_revision_id bigint, + ADD CONSTRAINT star_gift_catalog_collectible_revision_fk FOREIGN KEY (collectible_revision_id) + REFERENCES public.star_gift_collectible_revisions(id) ON DELETE RESTRICT; + +CREATE TABLE public.unique_star_gifts ( + id bigint DEFAULT nextval('public.unique_star_gift_id_seq') PRIMARY KEY, + gift_id bigint NOT NULL REFERENCES public.star_gift_catalog(gift_id) ON DELETE RESTRICT, + collectible_revision_id bigint NOT NULL REFERENCES public.star_gift_collectible_revisions(id) ON DELETE RESTRICT, + source_saved_gift_id bigint NOT NULL REFERENCES public.peer_star_gifts(id) ON DELETE RESTRICT, + title text DEFAULT '' NOT NULL, + slug text NOT NULL, + num integer NOT NULL, + owner_peer_type text NOT NULL, + owner_peer_id bigint NOT NULL, + model_attribute_id bigint NOT NULL, + pattern_attribute_id bigint NOT NULL, + backdrop_attribute_id bigint NOT NULL, + keep_original_details boolean DEFAULT false NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT unique_star_gift_slug_uniq UNIQUE (slug), + CONSTRAINT unique_star_gift_number_uniq UNIQUE (gift_id, num), + CONSTRAINT unique_star_gift_source_uniq UNIQUE (source_saved_gift_id), + CONSTRAINT unique_star_gift_owner_check CHECK (owner_peer_type IN ('user', 'channel') AND owner_peer_id > 0), + CONSTRAINT unique_star_gift_num_check CHECK (num > 0), + CONSTRAINT unique_star_gift_model_fk FOREIGN KEY (model_attribute_id, collectible_revision_id) + REFERENCES public.star_gift_collectible_models(id, collectible_revision_id) ON DELETE RESTRICT, + CONSTRAINT unique_star_gift_pattern_fk FOREIGN KEY (pattern_attribute_id, collectible_revision_id) + REFERENCES public.star_gift_collectible_patterns(id, collectible_revision_id) ON DELETE RESTRICT, + CONSTRAINT unique_star_gift_backdrop_fk FOREIGN KEY (backdrop_attribute_id, collectible_revision_id) + REFERENCES public.star_gift_collectible_backdrops(id, collectible_revision_id) ON DELETE RESTRICT +); + +ALTER TABLE public.peer_star_gifts + ADD COLUMN unique_gift_id bigint, + ADD COLUMN upgrade_msg_id integer DEFAULT 0 NOT NULL, + ADD COLUMN pinned_order integer DEFAULT 0 NOT NULL, + ADD CONSTRAINT peer_star_gifts_unique_gift_fk FOREIGN KEY (unique_gift_id) + REFERENCES public.unique_star_gifts(id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED, + ADD CONSTRAINT peer_star_gifts_terminal_state_check CHECK (NOT converted OR unique_gift_id IS NULL), + ADD CONSTRAINT peer_star_gifts_pinned_order_check CHECK (pinned_order >= 0); + +CREATE UNIQUE INDEX peer_star_gifts_unique_gift_uniq ON public.peer_star_gifts(unique_gift_id) + WHERE unique_gift_id IS NOT NULL; +CREATE INDEX unique_star_gifts_owner_idx ON public.unique_star_gifts(owner_peer_type, owner_peer_id, id DESC); + +CREATE TABLE public.star_gift_upgrade_commands ( + user_id bigint NOT NULL, + command_key text NOT NULL, + source_saved_gift_id bigint NOT NULL REFERENCES public.peer_star_gifts(id) ON DELETE RESTRICT, + form_id bigint DEFAULT 0 NOT NULL, + unique_gift_id bigint NOT NULL REFERENCES public.unique_star_gifts(id) ON DELETE RESTRICT, + balance_after bigint NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT star_gift_upgrade_commands_pkey PRIMARY KEY (user_id, command_key), + CONSTRAINT star_gift_upgrade_command_source_uniq UNIQUE (source_saved_gift_id) +); + +CREATE TABLE public.star_gift_collections ( + collection_id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + owner_peer_type text NOT NULL, + owner_peer_id bigint NOT NULL, + title text NOT NULL, + sort_order integer DEFAULT 0 NOT NULL, + hash bigint DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT star_gift_collection_owner_check CHECK (owner_peer_type IN ('user', 'channel') AND owner_peer_id > 0), + CONSTRAINT star_gift_collection_title_check CHECK (char_length(title) BETWEEN 1 AND 12), + CONSTRAINT star_gift_collection_owner_id_uniq UNIQUE (owner_peer_type, owner_peer_id, collection_id) +); + +CREATE INDEX star_gift_collections_owner_order_idx + ON public.star_gift_collections(owner_peer_type, owner_peer_id, sort_order, collection_id); + +CREATE TABLE public.star_gift_collection_items ( + collection_id integer NOT NULL REFERENCES public.star_gift_collections(collection_id) ON DELETE CASCADE, + saved_gift_id bigint NOT NULL REFERENCES public.peer_star_gifts(id) ON DELETE CASCADE, + sort_order integer DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT star_gift_collection_items_pkey PRIMARY KEY (collection_id, saved_gift_id) +); + +CREATE INDEX star_gift_collection_items_order_idx + ON public.star_gift_collection_items(collection_id, sort_order, saved_gift_id); +CREATE INDEX star_gift_collection_items_saved_idx + ON public.star_gift_collection_items(saved_gift_id, collection_id); + +-- Published definitions are immutable. The only permitted update is incrementing issued while +-- all definition columns remain byte-for-byte equal. +CREATE FUNCTION public.telesrv_guard_collectible_revision() RETURNS trigger + LANGUAGE plpgsql AS $$ +BEGIN + IF OLD.status = 'published' THEN + IF TG_OP = 'DELETE' THEN + RAISE EXCEPTION 'published collectible revision is immutable'; + END IF; + IF NEW.gift_id <> OLD.gift_id OR NEW.revision <> OLD.revision OR + NEW.upgrade_stars <> OLD.upgrade_stars OR NEW.supply_total <> OLD.supply_total OR + NEW.slug_prefix <> OLD.slug_prefix OR NEW.status <> OLD.status OR + NEW.created_by <> OLD.created_by OR NEW.command_id <> OLD.command_id OR + NEW.created_at <> OLD.created_at OR NEW.published_at <> OLD.published_at THEN + RAISE EXCEPTION 'published collectible revision is immutable'; + END IF; + END IF; + IF TG_OP = 'DELETE' THEN + RETURN OLD; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER star_gift_collectible_revision_guard + BEFORE UPDATE OR DELETE ON public.star_gift_collectible_revisions + FOR EACH ROW EXECUTE FUNCTION public.telesrv_guard_collectible_revision(); + +CREATE FUNCTION public.telesrv_guard_collectible_attribute() RETURNS trigger + LANGUAGE plpgsql AS $$ +DECLARE + revision_status text; +BEGIN + SELECT status INTO revision_status FROM public.star_gift_collectible_revisions + WHERE id = COALESCE(OLD.collectible_revision_id, NEW.collectible_revision_id); + IF revision_status = 'published' THEN + RAISE EXCEPTION 'published collectible attributes are immutable'; + END IF; + IF TG_OP = 'DELETE' THEN + RETURN OLD; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER star_gift_collectible_model_guard BEFORE UPDATE OR DELETE ON public.star_gift_collectible_models + FOR EACH ROW EXECUTE FUNCTION public.telesrv_guard_collectible_attribute(); +CREATE TRIGGER star_gift_collectible_pattern_guard BEFORE UPDATE OR DELETE ON public.star_gift_collectible_patterns + FOR EACH ROW EXECUTE FUNCTION public.telesrv_guard_collectible_attribute(); +CREATE TRIGGER star_gift_collectible_backdrop_guard BEFORE UPDATE OR DELETE ON public.star_gift_collectible_backdrops + FOR EACH ROW EXECUTE FUNCTION public.telesrv_guard_collectible_attribute(); diff --git a/deploy/migrations/0091_star_gift_prepaid_upgrade.down.sql b/deploy/migrations/0091_star_gift_prepaid_upgrade.down.sql new file mode 100644 index 00000000..bd759141 --- /dev/null +++ b/deploy/migrations/0091_star_gift_prepaid_upgrade.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE public.peer_star_gifts + DROP CONSTRAINT IF EXISTS peer_star_gifts_prepaid_upgrade_check, + DROP COLUMN IF EXISTS prepaid_upgrade_stars; diff --git a/deploy/migrations/0091_star_gift_prepaid_upgrade.up.sql b/deploy/migrations/0091_star_gift_prepaid_upgrade.up.sql new file mode 100644 index 00000000..44546724 --- /dev/null +++ b/deploy/migrations/0091_star_gift_prepaid_upgrade.up.sql @@ -0,0 +1,15 @@ +ALTER TABLE public.peer_star_gifts + ADD COLUMN IF NOT EXISTS prepaid_upgrade_stars bigint DEFAULT 0 NOT NULL; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conrelid = 'public.peer_star_gifts'::regclass + AND conname = 'peer_star_gifts_prepaid_upgrade_check' + ) THEN + ALTER TABLE public.peer_star_gifts + ADD CONSTRAINT peer_star_gifts_prepaid_upgrade_check CHECK (prepaid_upgrade_stars >= 0); + END IF; +END; +$$; diff --git a/deploy/migrations/0092_collectible_issuance_guard.down.sql b/deploy/migrations/0092_collectible_issuance_guard.down.sql new file mode 100644 index 00000000..9fbb3017 --- /dev/null +++ b/deploy/migrations/0092_collectible_issuance_guard.down.sql @@ -0,0 +1,21 @@ +CREATE OR REPLACE FUNCTION public.telesrv_guard_collectible_revision() RETURNS trigger + LANGUAGE plpgsql AS $$ +BEGIN + IF OLD.status = 'published' THEN + IF TG_OP = 'DELETE' THEN + RAISE EXCEPTION 'published collectible revision is immutable'; + END IF; + IF NEW.gift_id <> OLD.gift_id OR NEW.revision <> OLD.revision OR + NEW.upgrade_stars <> OLD.upgrade_stars OR NEW.supply_total <> OLD.supply_total OR + NEW.slug_prefix <> OLD.slug_prefix OR NEW.status <> OLD.status OR + NEW.created_by <> OLD.created_by OR NEW.command_id <> OLD.command_id OR + NEW.created_at <> OLD.created_at OR NEW.published_at <> OLD.published_at THEN + RAISE EXCEPTION 'published collectible revision is immutable'; + END IF; + END IF; + IF TG_OP = 'DELETE' THEN + RETURN OLD; + END IF; + RETURN NEW; +END; +$$; diff --git a/deploy/migrations/0092_collectible_issuance_guard.up.sql b/deploy/migrations/0092_collectible_issuance_guard.up.sql new file mode 100644 index 00000000..cca692d0 --- /dev/null +++ b/deploy/migrations/0092_collectible_issuance_guard.up.sql @@ -0,0 +1,28 @@ +-- Tighten the published collectible invariant. Issuance is the only mutable +-- field and every committed upgrade advances it exactly once. + +CREATE OR REPLACE FUNCTION public.telesrv_guard_collectible_revision() RETURNS trigger + LANGUAGE plpgsql AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + IF OLD.status = 'published' THEN + RAISE EXCEPTION 'published collectible revision is immutable'; + END IF; + RETURN OLD; + END IF; + + IF OLD.status = 'published' THEN + IF NEW.gift_id <> OLD.gift_id OR NEW.revision <> OLD.revision OR + NEW.upgrade_stars <> OLD.upgrade_stars OR NEW.supply_total <> OLD.supply_total OR + NEW.slug_prefix <> OLD.slug_prefix OR NEW.status <> OLD.status OR + NEW.created_by <> OLD.created_by OR NEW.command_id <> OLD.command_id OR + NEW.created_at <> OLD.created_at OR NEW.published_at <> OLD.published_at THEN + RAISE EXCEPTION 'published collectible revision is immutable'; + END IF; + IF NEW.issued <> OLD.issued + 1 THEN + RAISE EXCEPTION 'published collectible issuance must advance exactly once'; + END IF; + END IF; + RETURN NEW; +END; +$$; diff --git a/docs/configuration.en.md b/docs/configuration.en.md index 38a77e82..9cbaf73c 100644 --- a/docs/configuration.en.md +++ b/docs/configuration.en.md @@ -126,7 +126,7 @@ This document describes every setting loaded by `internal/config`. Defaults and | Setting | Type / code default | Description and constraints | |---|---|---| -| `TELESRV_BUSINESS_AI_PROVIDER` | string / `echo` | Business auto-reply generator: `echo`, `template`/`quick_reply`, or `ai`/`compose_ai`/a configured provider name. | +| `TELESRV_BUSINESS_AI_PROVIDER` | string / `echo` | Business auto-reply generator. Allowed values are `echo`/empty (echo the triggering text), `template`/`quick_reply`/`quick-reply` (use quick-reply templates), or `ai`/`compose_ai`/`ai_compose`/`aicompose`/`kimi` (reuse the `TELESRV_AI_PROVIDERS` provider chain). This setting does not accept arbitrary provider names; for example, with Ollama set `TELESRV_BUSINESS_AI_PROVIDER=ai` and select the actual provider through `TELESRV_AI_PROVIDERS=ollama,local`. | | `TELESRV_AI_ENABLED` | bool / `true` | Enables client compose rewrite/polish. False returns no tones and hides the entry. | | `TELESRV_AI_PROVIDERS` | list / `local` | Ordered provider chain. Empty resolves to deterministic `local`, which makes no external request. | | `TELESRV_AI_TIMEOUT` | duration / `15s` | Total timeout for one provider call. | diff --git a/docs/configuration.zh-CN.md b/docs/configuration.zh-CN.md index 0f0180d0..01265d36 100644 --- a/docs/configuration.zh-CN.md +++ b/docs/configuration.zh-CN.md @@ -126,7 +126,7 @@ | 参数 | 类型 / 代码默认值 | 说明与约束 | |---|---|---| -| `TELESRV_BUSINESS_AI_PROVIDER` | string / `echo` | Business 自动回复生成器:`echo`、`template`/`quick_reply`,或 `ai`/`compose_ai`/已配置 provider 名。 | +| `TELESRV_BUSINESS_AI_PROVIDER` | string / `echo` | Business 自动回复生成器。可填 `echo`/空值(回显触发文本)、`template`/`quick_reply`/`quick-reply`(使用 quick reply 模板),或 `ai`/`compose_ai`/`ai_compose`/`aicompose`/`kimi`(复用 `TELESRV_AI_PROVIDERS` provider 链)。这里不接受任意 provider 名;例如使用 Ollama 时填 `TELESRV_BUSINESS_AI_PROVIDER=ai`,实际 provider 由 `TELESRV_AI_PROVIDERS=ollama,local` 决定。 | | `TELESRV_AI_ENABLED` | bool / `true` | 启用客户端输入框改写/润色;关闭时返回空 tone 集合并隐藏入口。 | | `TELESRV_AI_PROVIDERS` | list / `local` | 按顺序尝试的 provider 链;空列表回退确定性 `local`,不访问外网。 | | `TELESRV_AI_TIMEOUT` | duration / `15s` | 单次 provider 调用总超时。 | diff --git a/internal/admin/service.go b/internal/admin/service.go index 3f08194a..8ee6b89f 100644 --- a/internal/admin/service.go +++ b/internal/admin/service.go @@ -2,10 +2,12 @@ package admin import ( "context" + "encoding/hex" "encoding/json" "fmt" "math" "net/url" + "reflect" "sort" "strings" "time" @@ -14,14 +16,18 @@ import ( ) const ( - ActionSetAccountFrozen = "account.set_frozen" - ActionGrantPremium = "account.grant_premium" - ActionGrantStars = "account.grant_stars" - ActionSetVerified = "account.set_verified" - ActionSetChannelVerified = "channel.set_verified" - ActionRevokeSessions = "account.revoke_sessions" - ActionDeletePrivateMessages = "messages.delete_private_messages" - ActionDeletePrivateHistory = "messages.delete_private_history" + ActionSetAccountFrozen = "account.set_frozen" + ActionGrantPremium = "account.grant_premium" + ActionGrantStars = "account.grant_stars" + ActionSetVerified = "account.set_verified" + ActionSetChannelVerified = "channel.set_verified" + ActionRevokeSessions = "account.revoke_sessions" + ActionDeletePrivateMessages = "messages.delete_private_messages" + ActionDeletePrivateHistory = "messages.delete_private_history" + ActionImportStarGift = "gifts.import" + ActionPublishGiftCollectibles = "gifts.collectibles.publish" + ActionSetStarGiftEnabled = "gifts.set_enabled" + ActionSetStarGiftSortOrder = "gifts.set_sort_order" maxCommandIDLength = 128 maxActorLength = 128 @@ -86,6 +92,17 @@ type MessagesService interface { DeleteHistory(ctx context.Context, userID int64, req domain.DeleteHistoryRequest) (domain.DeleteMessagesResult, error) } +type GiftsService interface { + PrepareAnimation(fileName string, data []byte) (domain.StarGiftAnimation, error) + CreateCatalogRevision(ctx context.Context, write domain.StarGiftCatalogWrite) (domain.StarGiftCatalogEntry, error) + SetCatalogEnabled(ctx context.Context, giftID int64, enabled bool) (bool, error) + SetCatalogSortOrder(ctx context.Context, giftID int64, sortOrder int) (bool, error) + AnimationJSON(ctx context.Context, giftID int64) ([]byte, bool, error) + CreateCollectibleRevision(ctx context.Context, write domain.StarGiftCollectibleWrite) (domain.StarGiftCollectibleRevision, error) + CollectiblePreview(ctx context.Context, giftID int64) (domain.StarGiftUpgradePreview, bool, error) + CollectibleAnimationJSON(ctx context.Context, giftID int64, kind domain.StarGiftCollectibleAttributeKind, attributeID int64) ([]byte, bool, error) +} + type Dependencies struct { Commands CommandRepository Restrictions RestrictionStore @@ -98,6 +115,7 @@ type Dependencies struct { Channels ChannelsService ChannelNotifier ChannelNotifier Messages MessagesService + Gifts GiftsService Now func() time.Time } @@ -113,6 +131,7 @@ type Service struct { channels ChannelsService channelNotifier ChannelNotifier messages MessagesService + gifts GiftsService now func() time.Time } @@ -155,6 +174,9 @@ func (s *Service) Configure(deps Dependencies) *Service { if deps.Messages != nil { s.messages = deps.Messages } + if deps.Gifts != nil { + s.gifts = deps.Gifts + } if deps.Now != nil { s.now = deps.Now } @@ -184,6 +206,63 @@ type CommandResult struct { Error string `json:"error,omitempty"` } +type ImportStarGiftRequest struct { + CommandMeta + GiftID int64 `json:"gift_id,omitempty"` + Title string `json:"title"` + Stars int64 `json:"stars"` + ConvertStars int64 `json:"convert_stars"` + Enabled bool `json:"enabled"` + SortOrder int `json:"sort_order"` + FileName string `json:"file_name"` + ContentSHA string `json:"content_sha256"` + Data []byte `json:"-"` +} + +type SetStarGiftEnabledRequest struct { + CommandMeta + GiftID int64 `json:"gift_id"` + Enabled bool `json:"enabled"` +} + +type SetStarGiftSortOrderRequest struct { + CommandMeta + GiftID int64 `json:"gift_id"` + SortOrder int `json:"sort_order"` +} + +type StarGiftCollectibleAnimationUpload struct { + Name string `json:"name"` + RarityPermille int `json:"rarity_permille"` + SortOrder int `json:"sort_order"` + FileKey string `json:"file_key"` + FileName string `json:"file_name,omitempty"` + ContentSHA string `json:"content_sha256,omitempty"` + Data []byte `json:"-"` +} + +type StarGiftCollectibleBackdropInput struct { + Name string `json:"name"` + BackdropID int `json:"backdrop_id"` + CenterColor int `json:"center_color"` + EdgeColor int `json:"edge_color"` + PatternColor int `json:"pattern_color"` + TextColor int `json:"text_color"` + RarityPermille int `json:"rarity_permille"` + SortOrder int `json:"sort_order"` +} + +type PublishStarGiftCollectiblesRequest struct { + CommandMeta + GiftID int64 `json:"gift_id"` + UpgradeStars int64 `json:"upgrade_stars"` + SupplyTotal int `json:"supply_total"` + SlugPrefix string `json:"slug_prefix"` + Models []StarGiftCollectibleAnimationUpload `json:"models"` + Patterns []StarGiftCollectibleAnimationUpload `json:"patterns"` + Backdrops []StarGiftCollectibleBackdropInput `json:"backdrops"` +} + type SetAccountFrozenRequest struct { CommandMeta UserID int64 `json:"user_id"` @@ -701,6 +780,182 @@ func (s *Service) DeletePrivateHistory(ctx context.Context, req DeletePrivateHis }) } +func (s *Service) ImportStarGift(ctx context.Context, req ImportStarGiftRequest) (CommandResult, error) { + if s == nil || s.gifts == nil { + return CommandResult{}, fmt.Errorf("star gift service is not configured") + } + if req.GiftID < 0 || req.Stars <= 0 || req.ConvertStars < 0 || req.ConvertStars > req.Stars || + req.SortOrder < math.MinInt32 || req.SortOrder > math.MaxInt32 || + len([]rune(strings.TrimSpace(req.Title))) > domain.MaxStarGiftTitleRunes { + return CommandResult{}, domain.ErrStarGiftInvalid + } + animation, err := s.gifts.PrepareAnimation(req.FileName, req.Data) + if err != nil { + return CommandResult{}, err + } + req.ContentSHA = hex.EncodeToString(animation.SHA256) + return s.runCommand(ctx, req.CommandMeta, ActionImportStarGift, 0, domain.Peer{}, req, func() (CommandResult, error) { + details := map[string]any{ + "gift_id": req.GiftID, "title": strings.TrimSpace(req.Title), "stars": req.Stars, + "convert_stars": req.ConvertStars, "enabled": req.Enabled, "sort_order": req.SortOrder, + "source_format": animation.SourceFormat, "source_name": animation.SourceName, + "sha256": req.ContentSHA, "width": animation.Width, "height": animation.Height, + "frame_rate": animation.FrameRate, "compressed_bytes": len(animation.TGS), "json_bytes": len(animation.JSON), + } + if req.DryRun { + return CommandResult{Message: "star gift import validated", Details: details}, nil + } + entry, err := s.gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{ + GiftID: req.GiftID, Title: req.Title, Stars: req.Stars, ConvertStars: req.ConvertStars, + Enabled: req.Enabled, SortOrder: req.SortOrder, Animation: animation, + Actor: req.Actor, CommandID: req.CommandID, + }) + if err != nil { + return CommandResult{Details: details}, err + } + details["gift_id"] = entry.Gift.ID + details["revision_id"] = entry.Gift.RevisionID + details["revision"] = entry.Revision + return CommandResult{Message: "star gift imported", Details: details}, nil + }) +} + +func (s *Service) PublishStarGiftCollectibles(ctx context.Context, req PublishStarGiftCollectiblesRequest) (CommandResult, error) { + if s == nil || s.gifts == nil { + return CommandResult{}, fmt.Errorf("star gift service is not configured") + } + toAttributes := func(kind domain.StarGiftCollectibleAttributeKind, uploads []StarGiftCollectibleAnimationUpload) ([]domain.StarGiftCollectibleAttribute, error) { + attributes := make([]domain.StarGiftCollectibleAttribute, len(uploads)) + for i := range uploads { + animation, err := s.gifts.PrepareAnimation(uploads[i].FileName, uploads[i].Data) + if err != nil { + return nil, fmt.Errorf("prepare %s %q: %w", kind, uploads[i].Name, err) + } + uploads[i].ContentSHA = hex.EncodeToString(animation.SHA256) + attributes[i] = domain.StarGiftCollectibleAttribute{ + Kind: kind, Name: strings.TrimSpace(uploads[i].Name), RarityPermille: uploads[i].RarityPermille, + SortOrder: uploads[i].SortOrder, Animation: &animation, + } + } + return attributes, nil + } + models, err := toAttributes(domain.StarGiftCollectibleModel, req.Models) + if err != nil { + return CommandResult{}, err + } + patterns, err := toAttributes(domain.StarGiftCollectiblePattern, req.Patterns) + if err != nil { + return CommandResult{}, err + } + backdrops := make([]domain.StarGiftCollectibleAttribute, len(req.Backdrops)) + for i, backdrop := range req.Backdrops { + backdrops[i] = domain.StarGiftCollectibleAttribute{ + Kind: domain.StarGiftCollectibleBackdrop, Name: strings.TrimSpace(backdrop.Name), BackdropID: backdrop.BackdropID, + CenterColor: backdrop.CenterColor, EdgeColor: backdrop.EdgeColor, PatternColor: backdrop.PatternColor, + TextColor: backdrop.TextColor, RarityPermille: backdrop.RarityPermille, SortOrder: backdrop.SortOrder, + } + } + write := domain.StarGiftCollectibleWrite{ + GiftID: req.GiftID, UpgradeStars: req.UpgradeStars, SupplyTotal: req.SupplyTotal, + SlugPrefix: strings.ToLower(strings.TrimSpace(req.SlugPrefix)), Models: models, Patterns: patterns, Backdrops: backdrops, + Actor: req.Actor, CommandID: req.CommandID, + } + if err := domain.ValidateStarGiftCollectibleDraft(write); err != nil { + return CommandResult{}, err + } + // Persist normalized content hashes in the command payload so retries with changed files are + // rejected by the shared idempotency boundary even though raw file bytes are not audit-logged. + for i := range req.Models { + req.Models[i].ContentSHA = hex.EncodeToString(models[i].Animation.SHA256) + } + for i := range req.Patterns { + req.Patterns[i].ContentSHA = hex.EncodeToString(patterns[i].Animation.SHA256) + } + return s.runCommand(ctx, req.CommandMeta, ActionPublishGiftCollectibles, 0, domain.Peer{}, req, func() (CommandResult, error) { + details := map[string]any{ + "gift_id": req.GiftID, "upgrade_stars": req.UpgradeStars, "supply_total": req.SupplyTotal, + "slug_prefix": write.SlugPrefix, "models": collectibleUploadDetails(req.Models), + "patterns": collectibleUploadDetails(req.Patterns), "backdrops": len(req.Backdrops), + } + if req.DryRun { + return CommandResult{Message: "star gift collectible pool validated", Details: details}, nil + } + revision, err := s.gifts.CreateCollectibleRevision(ctx, write) + if err != nil { + return CommandResult{Details: details}, err + } + details["revision_id"] = revision.ID + details["revision"] = revision.Revision + details["published"] = revision.Published + return CommandResult{Message: "star gift collectible pool published", Details: details}, nil + }) +} + +func collectibleUploadDetails(uploads []StarGiftCollectibleAnimationUpload) []map[string]any { + details := make([]map[string]any, 0, len(uploads)) + for _, upload := range uploads { + details = append(details, map[string]any{ + "name": strings.TrimSpace(upload.Name), "rarity_permille": upload.RarityPermille, + "sort_order": upload.SortOrder, "source_name": upload.FileName, "sha256": upload.ContentSHA, + }) + } + return details +} + +func (s *Service) SetStarGiftEnabled(ctx context.Context, req SetStarGiftEnabledRequest) (CommandResult, error) { + if s == nil || s.gifts == nil || req.GiftID <= 0 { + return CommandResult{}, fmt.Errorf("valid star gift and service are required") + } + return s.runCommand(ctx, req.CommandMeta, ActionSetStarGiftEnabled, 0, domain.Peer{}, req, func() (CommandResult, error) { + details := map[string]any{"gift_id": req.GiftID, "enabled": req.Enabled} + if req.DryRun { + return CommandResult{Message: "star gift state change validated", Details: details}, nil + } + changed, err := s.gifts.SetCatalogEnabled(ctx, req.GiftID, req.Enabled) + details["changed"] = changed + return CommandResult{Message: "star gift state updated", Details: details}, err + }) +} + +func (s *Service) SetStarGiftSortOrder(ctx context.Context, req SetStarGiftSortOrderRequest) (CommandResult, error) { + if s == nil || s.gifts == nil || req.GiftID <= 0 || req.SortOrder < math.MinInt32 || req.SortOrder > math.MaxInt32 { + return CommandResult{}, fmt.Errorf("valid star gift and service are required") + } + return s.runCommand(ctx, req.CommandMeta, ActionSetStarGiftSortOrder, 0, domain.Peer{}, req, func() (CommandResult, error) { + details := map[string]any{"gift_id": req.GiftID, "sort_order": req.SortOrder} + if req.DryRun { + return CommandResult{Message: "star gift order change validated", Details: details}, nil + } + changed, err := s.gifts.SetCatalogSortOrder(ctx, req.GiftID, req.SortOrder) + details["changed"] = changed + return CommandResult{Message: "star gift order updated", Details: details}, err + }) +} + +func (s *Service) StarGiftAnimation(ctx context.Context, giftID int64) ([]byte, bool, error) { + if s == nil || s.gifts == nil || giftID <= 0 { + return nil, false, nil + } + return s.gifts.AnimationJSON(ctx, giftID) +} + +func (s *Service) StarGiftCollectibles(ctx context.Context, giftID int64) (domain.StarGiftUpgradePreview, bool, error) { + if s == nil || s.gifts == nil || giftID <= 0 { + return domain.StarGiftUpgradePreview{}, false, nil + } + return s.gifts.CollectiblePreview(ctx, giftID) +} + +func (s *Service) StarGiftCollectibleAnimation(ctx context.Context, giftID int64, kind domain.StarGiftCollectibleAttributeKind, attributeID int64) ([]byte, bool, error) { + if s == nil || s.gifts == nil || giftID <= 0 || attributeID <= 0 { + return nil, false, nil + } + if kind != domain.StarGiftCollectibleModel && kind != domain.StarGiftCollectiblePattern { + return nil, false, domain.ErrStarGiftCollectibleInvalid + } + return s.gifts.CollectibleAnimationJSON(ctx, giftID, kind, attributeID) +} + func (s *Service) runCommand(ctx context.Context, meta CommandMeta, action string, targetUserID int64, targetPeer domain.Peer, request any, fn func() (CommandResult, error)) (CommandResult, error) { if s == nil || s.commands == nil { return CommandResult{}, fmt.Errorf("admin command store is not configured") @@ -737,6 +992,9 @@ func (s *Service) runCommand(ctx context.Context, meta CommandMeta, action strin return CommandResult{}, err } if !created { + if cmd.Action != action || cmd.DryRun != meta.DryRun || !sameJSON(cmd.RequestJSON, requestJSON) { + return CommandResult{CommandID: meta.CommandID, Action: action, Status: string(domain.AdminCommandFailed), Error: "COMMAND_ID_CONFLICT", Message: "command_id is already bound to a different request"}, fmt.Errorf("COMMAND_ID_CONFLICT") + } return resultFromCommand(cmd), nil } result, opErr := fn() @@ -770,6 +1028,14 @@ func (s *Service) runCommand(ctx context.Context, meta CommandMeta, action strin return result, opErr } +func sameJSON(a, b []byte) bool { + var left, right any + if json.Unmarshal(a, &left) != nil || json.Unmarshal(b, &right) != nil { + return string(a) == string(b) + } + return reflect.DeepEqual(left, right) +} + func resultFromCommand(cmd domain.AdminCommand) CommandResult { var result CommandResult if len(cmd.ResultJSON) > 0 { diff --git a/internal/admin/service_test.go b/internal/admin/service_test.go index 7b39b5f7..209f6d56 100644 --- a/internal/admin/service_test.go +++ b/internal/admin/service_test.go @@ -2,6 +2,7 @@ package admin import ( "context" + "crypto/sha256" "errors" "reflect" "strings" @@ -695,6 +696,95 @@ type fakeChannelNotifier struct { channels []int64 } +func TestImportStarGiftDryRunThenConfirm(t *testing.T) { + gifts := &fakeGiftsService{} + svc := NewService(Dependencies{Commands: newMemoryCommandRepo(), Gifts: gifts, Now: fixedNow}) + base := ImportStarGiftRequest{ + Title: "Cake", Stars: 50, ConvertStars: 25, Enabled: true, SortOrder: 3, + FileName: "cake.lottie", Data: []byte(`{"v":"5.7"}`), + } + base.CommandMeta = CommandMeta{CommandID: "dry-gift", Actor: "ops", Reason: "catalog", DryRun: true} + preview, err := svc.ImportStarGift(context.Background(), base) + if err != nil || gifts.createCalls != 0 || preview.Details["source_format"] != domain.StarGiftAnimationLottie { + t.Fatalf("preview=%+v err=%v create=%d", preview, err, gifts.createCalls) + } + base.CommandMeta = CommandMeta{CommandID: "exec-gift", Actor: "ops", Reason: "catalog", DryRun: false} + result, err := svc.ImportStarGift(context.Background(), base) + if err != nil || gifts.createCalls != 1 || result.Details["revision_id"] != int64(22) { + t.Fatalf("result=%+v err=%v create=%d", result, err, gifts.createCalls) + } +} + +func TestCommandIDConflictRejectsDifferentGiftBytes(t *testing.T) { + gifts := &fakeGiftsService{} + svc := NewService(Dependencies{Commands: newMemoryCommandRepo(), Gifts: gifts, Now: fixedNow}) + req := ImportStarGiftRequest{ + CommandMeta: CommandMeta{CommandID: "same", Actor: "ops", Reason: "catalog", DryRun: true}, + Title: "Gift", Stars: 10, ConvertStars: 5, Enabled: true, FileName: "a.lottie", Data: []byte("one"), + } + if _, err := svc.ImportStarGift(context.Background(), req); err != nil { + t.Fatal(err) + } + req.Data = []byte("two") + if _, err := svc.ImportStarGift(context.Background(), req); err == nil || err.Error() != "COMMAND_ID_CONFLICT" { + t.Fatalf("conflict err=%v", err) + } +} + +func TestPublishStarGiftCollectiblesDryRunThenConfirm(t *testing.T) { + gifts := &fakeGiftsService{} + svc := NewService(Dependencies{Commands: newMemoryCommandRepo(), Gifts: gifts, Now: fixedNow}) + base := PublishStarGiftCollectiblesRequest{ + GiftID: 11, UpgradeStars: 125, SupplyTotal: 100, SlugPrefix: "cake", + Models: []StarGiftCollectibleAnimationUpload{{Name: "Ruby", RarityPermille: 1000, FileKey: "model-0", FileName: "ruby.lottie", Data: []byte("model")}}, + Patterns: []StarGiftCollectibleAnimationUpload{{Name: "Stars", RarityPermille: 1000, FileKey: "pattern-0", FileName: "stars.tgs", Data: []byte("pattern")}}, + Backdrops: []StarGiftCollectibleBackdropInput{{Name: "Night", BackdropID: 1, CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, RarityPermille: 1000}}, + } + base.CommandMeta = CommandMeta{CommandID: "dry-collectibles", Actor: "ops", Reason: "pool", DryRun: true} + preview, err := svc.PublishStarGiftCollectibles(context.Background(), base) + if err != nil || gifts.createCalls != 0 || preview.Details["models"] == nil { + t.Fatalf("preview=%+v err=%v create=%d", preview, err, gifts.createCalls) + } + base.CommandMeta = CommandMeta{CommandID: "exec-collectibles", Actor: "ops", Reason: "pool", DryRun: false} + result, err := svc.PublishStarGiftCollectibles(context.Background(), base) + if err != nil || gifts.createCalls != 1 || result.Details["revision_id"] != int64(33) || result.Details["published"] != true { + t.Fatalf("result=%+v err=%v create=%d", result, err, gifts.createCalls) + } +} + +type fakeGiftsService struct{ createCalls int } + +func (f *fakeGiftsService) PrepareAnimation(name string, data []byte) (domain.StarGiftAnimation, error) { + sum := sha256.Sum256(data) + return domain.StarGiftAnimation{ + SourceName: name, SourceFormat: domain.StarGiftAnimationLottie, + JSON: []byte(`{"v":"5.7"}`), TGS: []byte("tgs"), SHA256: sum[:], Width: 512, Height: 512, FrameRate: 30, + }, nil +} +func (f *fakeGiftsService) CreateCatalogRevision(_ context.Context, write domain.StarGiftCatalogWrite) (domain.StarGiftCatalogEntry, error) { + f.createCalls++ + return domain.StarGiftCatalogEntry{Gift: domain.StarGift{ID: 11, RevisionID: 22, Stars: write.Stars}, Revision: 1}, nil +} +func (*fakeGiftsService) SetCatalogEnabled(context.Context, int64, bool) (bool, error) { + return true, nil +} +func (*fakeGiftsService) SetCatalogSortOrder(context.Context, int64, int) (bool, error) { + return true, nil +} +func (*fakeGiftsService) AnimationJSON(context.Context, int64) ([]byte, bool, error) { + return []byte(`{"v":"5.7"}`), true, nil +} +func (f *fakeGiftsService) CreateCollectibleRevision(_ context.Context, write domain.StarGiftCollectibleWrite) (domain.StarGiftCollectibleRevision, error) { + f.createCalls++ + return domain.StarGiftCollectibleRevision{ID: 33, GiftID: write.GiftID, Revision: 2, Published: true}, nil +} +func (*fakeGiftsService) CollectiblePreview(context.Context, int64) (domain.StarGiftUpgradePreview, bool, error) { + return domain.StarGiftUpgradePreview{}, false, nil +} +func (*fakeGiftsService) CollectibleAnimationJSON(context.Context, int64, domain.StarGiftCollectibleAttributeKind, int64) ([]byte, bool, error) { + return []byte(`{"v":"5.7"}`), true, nil +} + func (f *fakeChannelNotifier) NotifyChannelChanged(_ context.Context, ch domain.Channel) error { f.channels = append(f.channels, ch.ID) return nil diff --git a/internal/adminapi/server.go b/internal/adminapi/server.go index cc79ca74..0312bb71 100644 --- a/internal/adminapi/server.go +++ b/internal/adminapi/server.go @@ -5,13 +5,16 @@ import ( "crypto/subtle" "encoding/json" "fmt" + "io" "net/http" + "strconv" "strings" "time" "go.uber.org/zap" "telesrv/internal/admin" + "telesrv/internal/domain" ) type Config struct { @@ -28,6 +31,13 @@ type Service interface { RevokeSessions(ctx context.Context, req admin.RevokeSessionsRequest) (admin.CommandResult, error) DeletePrivateMessages(ctx context.Context, req admin.DeletePrivateMessagesRequest) (admin.CommandResult, error) DeletePrivateHistory(ctx context.Context, req admin.DeletePrivateHistoryRequest) (admin.CommandResult, error) + ImportStarGift(ctx context.Context, req admin.ImportStarGiftRequest) (admin.CommandResult, error) + PublishStarGiftCollectibles(ctx context.Context, req admin.PublishStarGiftCollectiblesRequest) (admin.CommandResult, error) + SetStarGiftEnabled(ctx context.Context, req admin.SetStarGiftEnabledRequest) (admin.CommandResult, error) + SetStarGiftSortOrder(ctx context.Context, req admin.SetStarGiftSortOrderRequest) (admin.CommandResult, error) + StarGiftAnimation(ctx context.Context, giftID int64) ([]byte, bool, error) + StarGiftCollectibles(ctx context.Context, giftID int64) (domain.StarGiftUpgradePreview, bool, error) + StarGiftCollectibleAnimation(ctx context.Context, giftID int64, kind domain.StarGiftCollectibleAttributeKind, attributeID int64) ([]byte, bool, error) } func Start(ctx context.Context, cfg Config, svc Service, log *zap.Logger) (*http.Server, error) { @@ -84,6 +94,13 @@ func (s *Server) routes() http.Handler { mux.HandleFunc("POST /v1/channels/set-verified", s.authenticated(s.handleSetChannelVerified)) mux.HandleFunc("POST /v1/messages/delete", s.authenticated(s.handleDeleteMessages)) mux.HandleFunc("POST /v1/messages/delete-history", s.authenticated(s.handleDeleteHistory)) + mux.HandleFunc("POST /v1/gifts/import", s.authenticated(s.handleImportStarGift)) + mux.HandleFunc("POST /v1/gifts/{id}/collectibles/publish", s.authenticated(s.handlePublishStarGiftCollectibles)) + mux.HandleFunc("POST /v1/gifts/set-enabled", s.authenticated(s.handleSetStarGiftEnabled)) + mux.HandleFunc("POST /v1/gifts/set-sort-order", s.authenticated(s.handleSetStarGiftSortOrder)) + mux.HandleFunc("GET /v1/gifts/{id}/animation", s.authenticated(s.handleStarGiftAnimation)) + mux.HandleFunc("GET /v1/gifts/{id}/collectibles", s.authenticated(s.handleStarGiftCollectibles)) + mux.HandleFunc("GET /v1/gifts/{id}/collectibles/{kind}/{attribute_id}/animation", s.authenticated(s.handleStarGiftCollectibleAnimation)) return mux } @@ -170,6 +187,222 @@ func (s *Server) handleDeleteHistory(w http.ResponseWriter, r *http.Request) { writeCommandResult(w, result, err) } +func (s *Server) handleImportStarGift(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + r.Body = http.MaxBytesReader(w, r.Body, 5<<20) + if err := r.ParseMultipartForm(1 << 20); err != nil { + writeError(w, http.StatusBadRequest, "invalid multipart form: "+err.Error()) + return + } + if r.MultipartForm != nil { + defer r.MultipartForm.RemoveAll() + } + var req admin.ImportStarGiftRequest + dec := json.NewDecoder(strings.NewReader(r.FormValue("metadata"))) + dec.DisallowUnknownFields() + if err := dec.Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid metadata: "+err.Error()) + return + } + file, header, err := r.FormFile("file") + if err != nil { + writeError(w, http.StatusBadRequest, "animation file is required") + return + } + defer file.Close() + data, err := io.ReadAll(io.LimitReader(file, (4<<20)+1)) + if err != nil || len(data) == 0 || len(data) > 4<<20 { + writeError(w, http.StatusBadRequest, "animation file is empty or too large") + return + } + req.FileName = header.Filename + req.Data = data + result, err := s.svc.ImportStarGift(r.Context(), req) + writeCommandResult(w, result, err) +} + +func (s *Server) handlePublishStarGiftCollectibles(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil || giftID <= 0 { + writeError(w, http.StatusBadRequest, "invalid gift id") + return + } + r.Body = http.MaxBytesReader(w, r.Body, 64<<20) + if err := r.ParseMultipartForm(8 << 20); err != nil { + writeError(w, http.StatusBadRequest, "invalid collectible multipart form: "+err.Error()) + return + } + if r.MultipartForm != nil { + defer r.MultipartForm.RemoveAll() + } + var req admin.PublishStarGiftCollectiblesRequest + dec := json.NewDecoder(strings.NewReader(r.FormValue("metadata"))) + dec.DisallowUnknownFields() + if err := dec.Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid metadata: "+err.Error()) + return + } + req.GiftID = giftID + seen := make(map[string]struct{}, len(req.Models)+len(req.Patterns)) + if len(req.Models)+len(req.Patterns) > 128 { + writeError(w, http.StatusBadRequest, "too many collectible animation files") + return + } + load := func(upload *admin.StarGiftCollectibleAnimationUpload) error { + upload.FileKey = strings.TrimSpace(upload.FileKey) + if upload.FileKey == "" { + return fmt.Errorf("animation file key is required") + } + if _, ok := seen[upload.FileKey]; ok { + return fmt.Errorf("duplicate animation file key %q", upload.FileKey) + } + seen[upload.FileKey] = struct{}{} + file, header, err := r.FormFile(upload.FileKey) + if err != nil { + return fmt.Errorf("animation file %q is required", upload.FileKey) + } + defer file.Close() + data, err := io.ReadAll(io.LimitReader(file, (4<<20)+1)) + if err != nil || len(data) == 0 || len(data) > 4<<20 { + return fmt.Errorf("animation file %q is empty or too large", upload.FileKey) + } + upload.FileName = header.Filename + upload.Data = data + return nil + } + for i := range req.Models { + if err := load(&req.Models[i]); err != nil { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + } + for i := range req.Patterns { + if err := load(&req.Patterns[i]); err != nil { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + } + result, err := s.svc.PublishStarGiftCollectibles(r.Context(), req) + writeCommandResult(w, result, err) +} + +func (s *Server) handleSetStarGiftEnabled(w http.ResponseWriter, r *http.Request) { + var req admin.SetStarGiftEnabledRequest + if !decodeJSON(w, r, &req) { + return + } + result, err := s.svc.SetStarGiftEnabled(r.Context(), req) + writeCommandResult(w, result, err) +} + +func (s *Server) handleSetStarGiftSortOrder(w http.ResponseWriter, r *http.Request) { + var req admin.SetStarGiftSortOrderRequest + if !decodeJSON(w, r, &req) { + return + } + result, err := s.svc.SetStarGiftSortOrder(r.Context(), req) + writeCommandResult(w, result, err) +} + +func (s *Server) handleStarGiftAnimation(w http.ResponseWriter, r *http.Request) { + giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil || giftID <= 0 { + writeError(w, http.StatusBadRequest, "invalid gift id") + return + } + raw, found, err := s.svc.StarGiftAnimation(r.Context(), giftID) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + if !found { + writeError(w, http.StatusNotFound, "gift animation not found") + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Cache-Control", "private, max-age=60") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(raw) +} + +func (s *Server) handleStarGiftCollectibles(w http.ResponseWriter, r *http.Request) { + giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + if err != nil || giftID <= 0 { + writeError(w, http.StatusBadRequest, "invalid gift id") + return + } + preview, found, err := s.svc.StarGiftCollectibles(r.Context(), giftID) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + if !found { + writeJSON(w, http.StatusOK, map[string]any{"found": false, "gift_id": giftID}) + return + } + writeJSON(w, http.StatusOK, collectiblePreviewResponse(preview)) +} + +func collectiblePreviewResponse(preview domain.StarGiftUpgradePreview) map[string]any { + attribute := func(value domain.StarGiftCollectibleAttribute) map[string]any { + result := map[string]any{ + "id": value.ID, "name": value.Name, "rarity_permille": value.RarityPermille, + "sort_order": value.SortOrder, "kind": value.Kind, + } + if value.Animation != nil { + result["source_name"] = value.Animation.SourceName + result["source_format"] = value.Animation.SourceFormat + } + if value.Kind == domain.StarGiftCollectibleBackdrop { + result["backdrop_id"] = value.BackdropID + result["center_color"] = value.CenterColor + result["edge_color"] = value.EdgeColor + result["pattern_color"] = value.PatternColor + result["text_color"] = value.TextColor + } + return result + } + mapAttributes := func(values []domain.StarGiftCollectibleAttribute) []map[string]any { + result := make([]map[string]any, 0, len(values)) + for _, value := range values { + result = append(result, attribute(value)) + } + return result + } + return map[string]any{ + "found": true, "gift_id": preview.GiftID, "revision": preview.Revision, "upgrade_stars": preview.UpgradeStars, + "supply_total": preview.SupplyTotal, "issued": preview.Issued, + "slug_prefix": preview.SlugPrefix, + "models": mapAttributes(preview.Models), "patterns": mapAttributes(preview.Patterns), + "backdrops": mapAttributes(preview.Backdrops), + } +} + +func (s *Server) handleStarGiftCollectibleAnimation(w http.ResponseWriter, r *http.Request) { + giftID, err := strconv.ParseInt(r.PathValue("id"), 10, 64) + attributeID, attrErr := strconv.ParseInt(r.PathValue("attribute_id"), 10, 64) + kind := domain.StarGiftCollectibleAttributeKind(r.PathValue("kind")) + if err != nil || giftID <= 0 || attrErr != nil || attributeID <= 0 || + (kind != domain.StarGiftCollectibleModel && kind != domain.StarGiftCollectiblePattern) { + writeError(w, http.StatusBadRequest, "invalid collectible animation") + return + } + raw, found, err := s.svc.StarGiftCollectibleAnimation(r.Context(), giftID, kind, attributeID) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + if !found { + writeError(w, http.StatusNotFound, "collectible animation not found") + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Cache-Control", "private, max-age=60") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(raw) +} + func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool { defer r.Body.Close() dec := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) diff --git a/internal/adminapi/server_test.go b/internal/adminapi/server_test.go index dc35347a..e031bc83 100644 --- a/internal/adminapi/server_test.go +++ b/internal/adminapi/server_test.go @@ -1,13 +1,16 @@ package adminapi import ( + "bytes" "context" + "mime/multipart" "net/http" "net/http/httptest" "strings" "testing" "telesrv/internal/admin" + "telesrv/internal/domain" ) func TestAdminAPIRequiresBearerToken(t *testing.T) { @@ -80,6 +83,74 @@ func TestAdminAPISetChannelVerified(t *testing.T) { } } +func TestAdminAPIImportStarGiftMultipart(t *testing.T) { + var body bytes.Buffer + writer := multipart.NewWriter(&body) + if err := writer.WriteField("metadata", `{"command_id":"gift-1","actor":"ops","reason":"catalog","dry_run":true,"title":"Gift","stars":50,"convert_stars":25,"enabled":true,"sort_order":3}`); err != nil { + t.Fatal(err) + } + part, err := writer.CreateFormFile("file", "gift.lottie") + if err != nil { + t.Fatal(err) + } + animation := []byte(`{"v":"5.7","w":512,"h":512,"fr":30,"ip":0,"op":30,"layers":[{}]}`) + if _, err := part.Write(animation); err != nil { + t.Fatal(err) + } + if err := writer.Close(); err != nil { + t.Fatal(err) + } + + svc := &captureGiftService{} + srv := &Server{token: "secret", svc: svc} + req := httptest.NewRequest(http.MethodPost, "/v1/gifts/import", &body) + req.Header.Set("Authorization", "Bearer secret") + req.Header.Set("Content-Type", writer.FormDataContentType()) + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + if svc.req.CommandID != "gift-1" || svc.req.FileName != "gift.lottie" || !bytes.Equal(svc.req.Data, animation) || svc.req.Stars != 50 || svc.req.ConvertStars != 25 { + t.Fatalf("decoded gift request = %+v", svc.req) + } +} + +func TestAdminAPIPublishStarGiftCollectiblesMultipart(t *testing.T) { + var body bytes.Buffer + writer := multipart.NewWriter(&body) + metadata := `{"command_id":"pool-1","actor":"ops","reason":"pool","dry_run":true,"upgrade_stars":125,"supply_total":100,"slug_prefix":"cake","models":[{"name":"Ruby","rarity_permille":1000,"sort_order":0,"file_key":"model-0"}],"patterns":[{"name":"Stars","rarity_permille":1000,"sort_order":0,"file_key":"pattern-0"}],"backdrops":[{"name":"Night","backdrop_id":1,"center_color":1122867,"edge_color":2241348,"pattern_color":3359829,"text_color":16777215,"rarity_permille":1000,"sort_order":0}]}` + if err := writer.WriteField("metadata", metadata); err != nil { + t.Fatal(err) + } + for key, name := range map[string]string{"model-0": "ruby.lottie", "pattern-0": "stars.tgs"} { + part, err := writer.CreateFormFile(key, name) + if err != nil { + t.Fatal(err) + } + if _, err := part.Write([]byte(key)); err != nil { + t.Fatal(err) + } + } + if err := writer.Close(); err != nil { + t.Fatal(err) + } + svc := &captureCollectibleService{} + srv := &Server{token: "secret", svc: svc} + req := httptest.NewRequest(http.MethodPost, "/v1/gifts/11/collectibles/publish", &body) + req.Header.Set("Authorization", "Bearer secret") + req.Header.Set("Content-Type", writer.FormDataContentType()) + rec := httptest.NewRecorder() + srv.routes().ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) + } + if svc.req.GiftID != 11 || len(svc.req.Models) != 1 || svc.req.Models[0].FileName != "ruby.lottie" || + string(svc.req.Patterns[0].Data) != "pattern-0" || len(svc.req.Backdrops) != 1 { + t.Fatalf("decoded collectible request = %+v", svc.req) + } +} + type fakeService struct{} type captureFreezeService struct { @@ -87,11 +158,31 @@ type captureFreezeService struct { req admin.SetAccountFrozenRequest } +type captureGiftService struct { + fakeService + req admin.ImportStarGiftRequest +} + +type captureCollectibleService struct { + fakeService + req admin.PublishStarGiftCollectiblesRequest +} + func (s *captureFreezeService) SetAccountFrozen(_ context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error) { s.req = req return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } +func (s *captureGiftService) ImportStarGift(_ context.Context, req admin.ImportStarGiftRequest) (admin.CommandResult, error) { + s.req = req + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + +func (s *captureCollectibleService) PublishStarGiftCollectibles(_ context.Context, req admin.PublishStarGiftCollectiblesRequest) (admin.CommandResult, error) { + s.req = req + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + func (fakeService) SetAccountFrozen(_ context.Context, req admin.SetAccountFrozenRequest) (admin.CommandResult, error) { return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil } @@ -123,3 +214,31 @@ func (fakeService) DeletePrivateMessages(context.Context, admin.DeletePrivateMes func (fakeService) DeletePrivateHistory(context.Context, admin.DeletePrivateHistoryRequest) (admin.CommandResult, error) { return admin.CommandResult{}, nil } + +func (fakeService) ImportStarGift(_ context.Context, req admin.ImportStarGiftRequest) (admin.CommandResult, error) { + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + +func (fakeService) PublishStarGiftCollectibles(_ context.Context, req admin.PublishStarGiftCollectiblesRequest) (admin.CommandResult, error) { + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + +func (fakeService) SetStarGiftEnabled(_ context.Context, req admin.SetStarGiftEnabledRequest) (admin.CommandResult, error) { + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + +func (fakeService) SetStarGiftSortOrder(_ context.Context, req admin.SetStarGiftSortOrderRequest) (admin.CommandResult, error) { + return admin.CommandResult{CommandID: req.CommandID, Status: "completed", DryRun: req.DryRun}, nil +} + +func (fakeService) StarGiftAnimation(context.Context, int64) ([]byte, bool, error) { + return []byte(`{"v":"5.7","w":512,"h":512}`), true, nil +} + +func (fakeService) StarGiftCollectibles(context.Context, int64) (domain.StarGiftUpgradePreview, bool, error) { + return domain.StarGiftUpgradePreview{}, false, nil +} + +func (fakeService) StarGiftCollectibleAnimation(context.Context, int64, domain.StarGiftCollectibleAttributeKind, int64) ([]byte, bool, error) { + return []byte(`{"v":"5.7","w":512,"h":512}`), true, nil +} diff --git a/internal/app/files/star_gifts_catalog.go b/internal/app/files/star_gifts_catalog.go deleted file mode 100644 index acd24499..00000000 --- a/internal/app/files/star_gifts_catalog.go +++ /dev/null @@ -1,97 +0,0 @@ -package files - -import ( - "context" - "fmt" - - "telesrv/internal/domain" -) - -// Star gift 目录:从已 seed 的 animated_emoji 集按 emoticon 精选贴纸文档合成(复用文档行与 -// blob,不复制字节),镜像 EnsureDefaultEmojiStatusSet。目录是静态的,不入库。 -// 礼物 ID 取明显隔离的常量段避免撞键。 - -const starGiftIDBase int64 = 8_888_000_000_000_000 - -type starGiftSeed struct { - id int64 - emoticon string - stars int64 - title string -} - -// starGiftSeeds 是固定礼物目录(emoticon 需在 animated_emoji 集里,否则该礼物被跳过)。 -// convert_stars = stars(v1 全额转换,视作用新购 Stars 买入)。 -var starGiftSeeds = []starGiftSeed{ - {starGiftIDBase + 1, "❤", 15, "Heart"}, - {starGiftIDBase + 2, "\U0001f382", 50, "Cake"}, // 🎂 - {starGiftIDBase + 3, "\U0001f389", 100, "Party"}, // 🎉 - {starGiftIDBase + 4, "\U0001f525", 250, "Fire"}, // 🔥 - {starGiftIDBase + 5, "\U0001f3c6", 500, "Trophy"}, // 🏆 - {starGiftIDBase + 6, "\U0001f48e", 1000, "Diamond"}, // 💎 - {starGiftIDBase + 7, "\U0001f680", 2500, "Rocket"}, // 🚀 -} - -// BuildStarGiftCatalog 合成可购买礼物目录:解析每个 seed emoticon 的贴纸文档,跳过未 seed 的。 -// animated_emoji 未 seed 时返回空目录(客户端显示空礼物面板,购买流仍可对已知 gift_id 工作)。 -func (s *Service) BuildStarGiftCatalog(ctx context.Context) ([]domain.StarGift, error) { - source, found, err := s.media.GetStickerSetBySystemKey(ctx, "animated_emoji") - if err != nil { - return nil, fmt.Errorf("lookup animated_emoji set for star gifts: %w", err) - } - if !found || len(source.Packs) == 0 { - return nil, nil - } - byEmoticon := make(map[string]int64, len(source.Packs)) - for _, pack := range source.Packs { - key := normalizeStatusEmoticon(pack.Emoticon) - if key == "" || len(pack.DocumentIDs) == 0 { - continue - } - if _, ok := byEmoticon[key]; !ok { - byEmoticon[key] = pack.DocumentIDs[0] - } - } - // 收集要加载的文档 id(去重)。 - docIDs := make([]int64, 0, len(starGiftSeeds)) - chosen := make([]starGiftSeed, 0, len(starGiftSeeds)) - seen := make(map[int64]struct{}) - for _, seed := range starGiftSeeds { - id, ok := byEmoticon[normalizeStatusEmoticon(seed.emoticon)] - if !ok || id == 0 { - continue - } - chosen = append(chosen, seed) - if _, dup := seen[id]; !dup { - seen[id] = struct{}{} - docIDs = append(docIDs, id) - } - } - if len(chosen) == 0 { - return nil, nil - } - docs, err := s.media.GetDocuments(ctx, docIDs) - if err != nil { - return nil, fmt.Errorf("load star gift sticker documents: %w", err) - } - docByID := make(map[int64]domain.Document, len(docs)) - for _, d := range docs { - docByID[d.ID] = d - } - catalog := make([]domain.StarGift, 0, len(chosen)) - for _, seed := range chosen { - id := byEmoticon[normalizeStatusEmoticon(seed.emoticon)] - doc, ok := docByID[id] - if !ok || doc.ID == 0 { - continue - } - catalog = append(catalog, domain.StarGift{ - ID: seed.id, - Stars: seed.stars, - ConvertStars: seed.stars, - Title: seed.title, - Sticker: doc, - }) - } - return catalog, nil -} diff --git a/internal/app/stargifts/animation.go b/internal/app/stargifts/animation.go new file mode 100644 index 00000000..b8d23ea4 --- /dev/null +++ b/internal/app/stargifts/animation.go @@ -0,0 +1,190 @@ +package stargifts + +import ( + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "math" + "path/filepath" + "strings" + "time" + + "telesrv/internal/domain" +) + +// PrepareAnimation normalizes a .tgs or plain Lottie JSON (.json/.lottie) into the +// single canonical pair used by both the Telegram download path and admin preview. +func (s *Service) PrepareAnimation(fileName string, data []byte) (domain.StarGiftAnimation, error) { + return prepareAnimation(fileName, data) +} + +func prepareAnimation(fileName string, data []byte) (domain.StarGiftAnimation, error) { + fileName = strings.TrimSpace(filepath.Base(fileName)) + ext := strings.ToLower(filepath.Ext(fileName)) + format := domain.StarGiftAnimationLottie + var rawJSON []byte + if ext == ".tgs" || isGzip(data) { + format = domain.StarGiftAnimationTGS + if int64(len(data)) == 0 || int64(len(data)) > domain.MaxStarGiftTGSBytes { + return domain.StarGiftAnimation{}, domain.ErrStarGiftFileInvalid + } + var err error + rawJSON, err = decompressSingleTGS(data) + if err != nil { + return domain.StarGiftAnimation{}, err + } + } else { + if ext != ".json" && ext != ".lottie" { + return domain.StarGiftAnimation{}, fmt.Errorf("%w: expected .tgs, .json or plain .lottie", domain.ErrStarGiftFileInvalid) + } + if int64(len(data)) == 0 || int64(len(data)) > domain.MaxStarGiftLottieBytes { + return domain.StarGiftAnimation{}, domain.ErrStarGiftFileInvalid + } + rawJSON = data + } + + normalized, meta, err := normalizeAndValidateLottie(rawJSON) + if err != nil { + return domain.StarGiftAnimation{}, err + } + tgs, err := gzipLottie(normalized) + if err != nil || int64(len(tgs)) > domain.MaxStarGiftTGSBytes { + return domain.StarGiftAnimation{}, domain.ErrStarGiftFileInvalid + } + sum := sha256.Sum256(tgs) + return domain.StarGiftAnimation{ + SourceName: fileName, + SourceFormat: format, + JSON: normalized, + TGS: tgs, + SHA256: append([]byte(nil), sum[:]...), + Width: meta.W, + Height: meta.H, + FrameRate: meta.FrameRate, + InPoint: meta.InPoint, + OutPoint: meta.OutPoint, + }, nil +} + +type lottieMetadata struct { + Version string `json:"v"` + W int `json:"w"` + H int `json:"h"` + FrameRate float64 `json:"fr"` + InPoint float64 `json:"ip"` + OutPoint float64 `json:"op"` + Layers []json.RawMessage `json:"layers"` + Assets []json.RawMessage `json:"assets"` +} + +func normalizeAndValidateLottie(data []byte) ([]byte, lottieMetadata, error) { + data = bytes.TrimSpace(bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF})) + if len(data) == 0 || int64(len(data)) > domain.MaxStarGiftLottieBytes || !json.Valid(data) { + return nil, lottieMetadata{}, domain.ErrStarGiftFileInvalid + } + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + var root any + if err := dec.Decode(&root); err != nil { + return nil, lottieMetadata{}, domain.ErrStarGiftFileInvalid + } + if _, ok := root.(map[string]any); !ok { + return nil, lottieMetadata{}, domain.ErrStarGiftFileInvalid + } + if containsLottieExpression(root) { + return nil, lottieMetadata{}, fmt.Errorf("%w: expressions are not allowed", domain.ErrStarGiftFileInvalid) + } + var meta lottieMetadata + if err := json.Unmarshal(data, &meta); err != nil { + return nil, lottieMetadata{}, domain.ErrStarGiftFileInvalid + } + frameSpan := meta.OutPoint - meta.InPoint + if meta.Version == "" || meta.W != 512 || meta.H != 512 || + math.IsNaN(meta.FrameRate) || math.IsInf(meta.FrameRate, 0) || meta.FrameRate <= 0 || meta.FrameRate > domain.MaxStarGiftAnimationFrameRate || + math.IsNaN(meta.InPoint) || math.IsInf(meta.InPoint, 0) || meta.InPoint < 0 || + math.IsNaN(meta.OutPoint) || math.IsInf(meta.OutPoint, 0) || meta.OutPoint <= meta.InPoint || + frameSpan > meta.FrameRate*domain.MaxStarGiftAnimationSeconds || len(meta.Layers) == 0 { + return nil, lottieMetadata{}, domain.ErrStarGiftFileInvalid + } + // Telegram animated stickers are self-contained. Reject remote or embedded image assets; + // pre-composition assets with only an id/layers payload remain valid. + for _, raw := range meta.Assets { + var asset map[string]json.RawMessage + if json.Unmarshal(raw, &asset) != nil { + return nil, lottieMetadata{}, domain.ErrStarGiftFileInvalid + } + for _, key := range []string{"p", "u"} { + if value := asset[key]; len(value) > 0 && string(value) != `""` && string(value) != "null" { + return nil, lottieMetadata{}, fmt.Errorf("%w: external assets are not allowed", domain.ErrStarGiftFileInvalid) + } + } + } + var compact bytes.Buffer + if err := json.Compact(&compact, data); err != nil || int64(compact.Len()) > domain.MaxStarGiftLottieBytes { + return nil, lottieMetadata{}, domain.ErrStarGiftFileInvalid + } + return compact.Bytes(), meta, nil +} + +func containsLottieExpression(value any) bool { + switch node := value.(type) { + case map[string]any: + for key, child := range node { + if key == "x" { + if expression, ok := child.(string); ok && strings.TrimSpace(expression) != "" { + return true + } + } + if containsLottieExpression(child) { + return true + } + } + case []any: + for _, child := range node { + if containsLottieExpression(child) { + return true + } + } + } + return false +} + +func isGzip(data []byte) bool { + return len(data) >= 2 && data[0] == 0x1f && data[1] == 0x8b +} + +func decompressSingleTGS(data []byte) ([]byte, error) { + reader := bytes.NewReader(data) + gz, err := gzip.NewReader(reader) + if err != nil { + return nil, domain.ErrStarGiftFileInvalid + } + gz.Multistream(false) + raw, readErr := io.ReadAll(io.LimitReader(gz, domain.MaxStarGiftLottieBytes+1)) + closeErr := gz.Close() + if readErr != nil || closeErr != nil || int64(len(raw)) > domain.MaxStarGiftLottieBytes || reader.Len() != 0 { + return nil, domain.ErrStarGiftFileInvalid + } + return raw, nil +} + +func gzipLottie(data []byte) ([]byte, error) { + var out bytes.Buffer + gz, err := gzip.NewWriterLevel(&out, gzip.BestCompression) + if err != nil { + return nil, err + } + gz.Header.ModTime = time.Unix(0, 0) + gz.Header.OS = 255 + if _, err := gz.Write(data); err != nil { + _ = gz.Close() + return nil, err + } + if err := gz.Close(); err != nil { + return nil, err + } + return out.Bytes(), nil +} diff --git a/internal/app/stargifts/animation_test.go b/internal/app/stargifts/animation_test.go new file mode 100644 index 00000000..c21794ca --- /dev/null +++ b/internal/app/stargifts/animation_test.go @@ -0,0 +1,93 @@ +package stargifts + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "testing" + + "telesrv/internal/domain" + "telesrv/internal/store/memory" +) + +const validGiftLottie = `{"v":"5.7.4","fr":30,"ip":0,"op":60,"w":512,"h":512,"layers":[{"ty":4,"nm":"gift"}],"assets":[]}` + +func TestPrepareAnimationNormalizesLottieAndTGS(t *testing.T) { + fromJSON, err := prepareAnimation("gift.lottie", []byte(" \n"+validGiftLottie+"\n")) + if err != nil { + t.Fatalf("prepare lottie: %v", err) + } + if fromJSON.SourceFormat != domain.StarGiftAnimationLottie || len(fromJSON.TGS) == 0 || fromJSON.Width != 512 || fromJSON.Height != 512 { + t.Fatalf("prepared lottie = %+v", fromJSON) + } + fromTGS, err := prepareAnimation("gift.tgs", fromJSON.TGS) + if err != nil { + t.Fatalf("prepare tgs: %v", err) + } + if fromTGS.SourceFormat != domain.StarGiftAnimationTGS || string(fromTGS.JSON) != string(fromJSON.JSON) || hex.EncodeToString(fromTGS.SHA256) != hex.EncodeToString(fromJSON.SHA256) { + t.Fatalf("tgs round trip differs: json=%v hash=%x/%x", string(fromTGS.JSON) == string(fromJSON.JSON), fromTGS.SHA256, fromJSON.SHA256) + } +} + +func TestPrepareAnimationRejectsExternalAssetAndExpression(t *testing.T) { + for name, raw := range map[string]string{ + "external": `{"v":"5.7","fr":30,"ip":0,"op":30,"w":512,"h":512,"layers":[{}],"assets":[{"p":"https://example.test/x.png"}]}`, + "expression": `{"v":"5.7","fr":30,"ip":0,"op":30,"w":512,"h":512,"layers":[{"ks":{"o":{"x":"time*10"}}}]}`, + "wrong-size": `{"v":"5.7","fr":30,"ip":0,"op":30,"w":256,"h":256,"layers":[{}]}`, + "frame-rate": `{"v":"5.7","fr":121,"ip":0,"op":30,"w":512,"h":512,"layers":[{}]}`, + "duration": `{"v":"5.7","fr":30,"ip":0,"op":901,"w":512,"h":512,"layers":[{}]}`, + } { + t.Run(name, func(t *testing.T) { + if _, err := prepareAnimation("gift.json", []byte(raw)); !errors.Is(err, domain.ErrStarGiftFileInvalid) { + t.Fatalf("err=%v, want ErrStarGiftFileInvalid", err) + } + }) + } +} + +type testGiftBlob struct{ data map[string][]byte } + +func (b *testGiftBlob) Name() string { return "localfs" } +func (b *testGiftBlob) Put(_ context.Context, data []byte) (string, error) { + sum := sha256.Sum256(data) + key := hex.EncodeToString(sum[:]) + b.data[key] = append([]byte(nil), data...) + return key, nil +} +func (b *testGiftBlob) Get(_ context.Context, key string) ([]byte, error) { + return append([]byte(nil), b.data[key]...), nil +} + +func TestCreateCatalogRevisionPreservesHistoricalRevision(t *testing.T) { + ctx := context.Background() + store := memory.NewStarGiftStore() + svc := NewService(store, &testGiftBlob{data: map[string][]byte{}}, 2) + animation, err := svc.PrepareAnimation("gift.json", []byte(validGiftLottie)) + if err != nil { + t.Fatal(err) + } + first, err := svc.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{ + Stars: 50, ConvertStars: 25, Enabled: true, SortOrder: 1, Title: "First", Animation: animation, + }) + if err != nil { + t.Fatalf("create first: %v", err) + } + second, err := svc.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{ + GiftID: first.Gift.ID, Stars: 80, ConvertStars: 40, Enabled: true, SortOrder: 1, Title: "Second", Animation: animation, + }) + if err != nil { + t.Fatalf("create second: %v", err) + } + current, found, _ := svc.GiftByID(ctx, first.Gift.ID) + if !found || current.RevisionID != second.Gift.RevisionID || current.Stars != 80 { + t.Fatalf("current=%+v found=%v", current, found) + } + historical, found, _ := svc.GiftRevisionByID(ctx, first.Gift.RevisionID) + if !found || historical.Stars != 50 || historical.Title != "First" { + t.Fatalf("historical=%+v found=%v", historical, found) + } + if _, err := svc.SetCatalogEnabled(ctx, first.Gift.ID+999, false); !errors.Is(err, domain.ErrStarGiftNotFound) { + t.Fatalf("disable missing err=%v, want ErrStarGiftNotFound", err) + } +} diff --git a/internal/app/stargifts/service.go b/internal/app/stargifts/service.go index 7de7c932..43309748 100644 --- a/internal/app/stargifts/service.go +++ b/internal/app/stargifts/service.go @@ -1,124 +1,413 @@ -// Package stargifts 实现 Star 礼物应用服务:礼物目录(从 seed 合成、懒加载缓存)+ peer 收到的 -// 礼物实例 CRUD。扣费/退款/服务消息投递由 rpc 层编排(复用 Stars 账本 + SendPrivateText), -// 本层只管目录与持久化。 +// Package stargifts implements the durable Star Gift catalog and received-gift state. package stargifts import ( "context" + "crypto/rand" + "encoding/binary" + "fmt" + "strings" "sync" + "time" "telesrv/internal/domain" "telesrv/internal/store" ) -// CatalogProvider 合成礼物目录(app/files 实现)。 -type CatalogProvider interface { - BuildStarGiftCatalog(ctx context.Context) ([]domain.StarGift, error) +// BlobBackend is the content-addressed media boundary used by the catalog importer. +type BlobBackend interface { + Name() string + Put(ctx context.Context, data []byte) (string, error) + Get(ctx context.Context, objectKey string) ([]byte, error) } -// Service 是 Star 礼物应用服务。 type Service struct { - store store.StarGiftStore - catalog CatalogProvider + store store.StarGiftStore + upgrades store.StarGiftUpgradeStore + blobs BlobBackend + dc int - mu sync.Mutex + mu sync.RWMutex built bool gifts []domain.StarGift byID map[int64]domain.StarGift hash int } -// NewService 创建 Star 礼物服务。 -func NewService(st store.StarGiftStore, catalog CatalogProvider) *Service { - return &Service{store: st, catalog: catalog} +type Option func(*Service) + +func WithUpgradeStore(upgrades store.StarGiftUpgradeStore) Option { + return func(service *Service) { service.upgrades = upgrades } +} + +func NewService(st store.StarGiftStore, blobs BlobBackend, dc int, opts ...Option) *Service { + service := &Service{store: st, blobs: blobs, dc: dc} + for _, opt := range opts { + opt(service) + } + return service } -// ensureCatalog 懒加载并缓存目录(静态数据,构建一次)。 func (s *Service) ensureCatalog(ctx context.Context) error { + s.mu.RLock() + built := s.built + s.mu.RUnlock() + if built { + return nil + } s.mu.Lock() defer s.mu.Unlock() if s.built { return nil } - gifts, err := s.catalog.BuildStarGiftCatalog(ctx) + if s.store == nil { + return fmt.Errorf("star gift store is not configured") + } + gifts, err := s.store.Catalog(ctx) if err != nil { return err } s.gifts = gifts s.byID = make(map[int64]domain.StarGift, len(gifts)) - for _, g := range gifts { - s.byID[g.ID] = g + for _, gift := range gifts { + s.byID[gift.ID] = gift } s.hash = domain.StarGiftCatalogHash(gifts) s.built = true return nil } -// Catalog 返回礼物目录。 func (s *Service) Catalog(ctx context.Context) ([]domain.StarGift, error) { if err := s.ensureCatalog(ctx); err != nil { return nil, err } - s.mu.Lock() - defer s.mu.Unlock() - out := make([]domain.StarGift, len(s.gifts)) - copy(out, s.gifts) - return out, nil + s.mu.RLock() + defer s.mu.RUnlock() + return append([]domain.StarGift(nil), s.gifts...), nil } -// CatalogHash 返回目录 hash(getStarGifts NotModified 判定)。 func (s *Service) CatalogHash(ctx context.Context) (int, error) { if err := s.ensureCatalog(ctx); err != nil { return 0, err } - s.mu.Lock() - defer s.mu.Unlock() + s.mu.RLock() + defer s.mu.RUnlock() return s.hash, nil } -// GiftByID 返回目录中指定礼物,不存在返回 ok=false。 func (s *Service) GiftByID(ctx context.Context, id int64) (domain.StarGift, bool, error) { if err := s.ensureCatalog(ctx); err != nil { return domain.StarGift{}, false, err } - s.mu.Lock() - defer s.mu.Unlock() - g, ok := s.byID[id] - return g, ok, nil + s.mu.RLock() + defer s.mu.RUnlock() + gift, ok := s.byID[id] + return gift, ok, nil +} + +func (s *Service) GiftRevisionByID(ctx context.Context, revisionID int64) (domain.StarGift, bool, error) { + if s == nil || s.store == nil { + return domain.StarGift{}, false, nil + } + return s.store.CatalogRevision(ctx, revisionID) +} + +// InvalidateStarGiftCatalog implements the shared PostgreSQL read-model listener boundary. +func (s *Service) InvalidateStarGiftCatalog() { + if s == nil { + return + } + s.mu.Lock() + s.built = false + s.gifts = nil + s.byID = nil + s.hash = 0 + s.mu.Unlock() +} + +func (s *Service) FlushStarGiftCatalog() { s.InvalidateStarGiftCatalog() } + +func (s *Service) CreateCatalogRevision(ctx context.Context, write domain.StarGiftCatalogWrite) (domain.StarGiftCatalogEntry, error) { + if s == nil || s.store == nil || s.blobs == nil { + return domain.StarGiftCatalogEntry{}, fmt.Errorf("star gift catalog importer is not configured") + } + write.Title = strings.TrimSpace(write.Title) + if write.Stars <= 0 || write.ConvertStars < 0 || write.ConvertStars > write.Stars || + write.Animation.Width != 512 || write.Animation.Height != 512 || len(write.Animation.TGS) == 0 || + len([]rune(write.Title)) > domain.MaxStarGiftTitleRunes { + return domain.StarGiftCatalogEntry{}, domain.ErrStarGiftInvalid + } + objectKey, err := s.blobs.Put(ctx, write.Animation.TGS) + if err != nil { + return domain.StarGiftCatalogEntry{}, fmt.Errorf("store star gift animation: %w", err) + } + documentID, err := randomPositiveInt64() + if err != nil { + return domain.StarGiftCatalogEntry{}, err + } + accessHash, err := randomPositiveInt64() + if err != nil { + return domain.StarGiftCatalogEntry{}, err + } + fileReference := make([]byte, 16) + if _, err := rand.Read(fileReference); err != nil { + return domain.StarGiftCatalogEntry{}, fmt.Errorf("generate star gift file reference: %w", err) + } + write.Document = domain.Document{ + ID: documentID, + AccessHash: accessHash, + FileReference: fileReference, + Date: int(time.Now().Unix()), + MimeType: "application/x-tgsticker", + Size: int64(len(write.Animation.TGS)), + DCID: s.dc, + Attributes: []domain.DocumentAttribute{ + {Kind: domain.DocAttrImageSize, W: 512, H: 512}, + {Kind: domain.DocAttrSticker, Alt: "🎁"}, + {Kind: domain.DocAttrFilename, FileName: "gift.tgs"}, + }, + } + write.Blob = domain.FileBlob{ + LocationKey: fmt.Sprintf("doc:%d", documentID), + Backend: domain.MediaBackend(s.blobs.Name()), + ObjectKey: objectKey, + Size: int64(len(write.Animation.TGS)), + SHA256: append([]byte(nil), write.Animation.SHA256...), + MimeType: "application/x-tgsticker", + } + entry, err := s.store.CreateCatalogRevision(ctx, write) + if err != nil { + return domain.StarGiftCatalogEntry{}, err + } + s.InvalidateStarGiftCatalog() + return entry, nil +} + +func (s *Service) SetCatalogEnabled(ctx context.Context, giftID int64, enabled bool) (bool, error) { + changed, err := s.store.SetCatalogEnabled(ctx, giftID, enabled) + if err == nil { + s.InvalidateStarGiftCatalog() + } + return changed, err +} + +func (s *Service) SetCatalogSortOrder(ctx context.Context, giftID int64, sortOrder int) (bool, error) { + changed, err := s.store.SetCatalogSortOrder(ctx, giftID, sortOrder) + if err == nil { + s.InvalidateStarGiftCatalog() + } + return changed, err +} + +func (s *Service) AnimationJSON(ctx context.Context, giftID int64) ([]byte, bool, error) { + return s.store.AnimationJSON(ctx, giftID) +} + +func (s *Service) PublishCollectibleRevision(ctx context.Context, write domain.StarGiftCollectibleWrite) (domain.StarGiftCollectibleRevision, error) { + if s == nil || s.store == nil { + return domain.StarGiftCollectibleRevision{}, fmt.Errorf("star gift collectible store is not configured") + } + revision, err := s.store.PublishCollectibleRevision(ctx, write) + if err == nil { + s.InvalidateStarGiftCatalog() + } + return revision, err +} + +// CreateCollectibleRevision materializes the normalized model/pattern animations and then +// atomically publishes the complete immutable attribute pool. Callers must pass animations +// produced by PrepareAnimation; partial revisions are never exposed to clients. +func (s *Service) CreateCollectibleRevision(ctx context.Context, write domain.StarGiftCollectibleWrite) (domain.StarGiftCollectibleRevision, error) { + if s == nil || s.store == nil || s.blobs == nil { + return domain.StarGiftCollectibleRevision{}, fmt.Errorf("star gift collectible importer is not configured") + } + write.SlugPrefix = strings.ToLower(strings.TrimSpace(write.SlugPrefix)) + if err := domain.ValidateStarGiftCollectibleDraft(write); err != nil { + return domain.StarGiftCollectibleRevision{}, err + } + materialize := func(attributes []domain.StarGiftCollectibleAttribute) error { + for i := range attributes { + animation := attributes[i].Animation + if animation == nil { + return domain.ErrStarGiftCollectibleInvalid + } + objectKey, err := s.blobs.Put(ctx, animation.TGS) + if err != nil { + return fmt.Errorf("store collectible %s animation: %w", attributes[i].Kind, err) + } + documentID, err := randomPositiveInt64() + if err != nil { + return err + } + accessHash, err := randomPositiveInt64() + if err != nil { + return err + } + fileReference := make([]byte, 16) + if _, err := rand.Read(fileReference); err != nil { + return fmt.Errorf("generate collectible file reference: %w", err) + } + attributes[i].Document = &domain.Document{ + ID: documentID, AccessHash: accessHash, FileReference: fileReference, + Date: int(time.Now().Unix()), MimeType: "application/x-tgsticker", + Size: int64(len(animation.TGS)), DCID: s.dc, + Attributes: []domain.DocumentAttribute{ + {Kind: domain.DocAttrImageSize, W: 512, H: 512}, + {Kind: domain.DocAttrSticker, Alt: "🎁"}, + {Kind: domain.DocAttrFilename, FileName: string(attributes[i].Kind) + ".tgs"}, + }, + } + attributes[i].Blob = &domain.FileBlob{ + LocationKey: fmt.Sprintf("doc:%d", documentID), Backend: domain.MediaBackend(s.blobs.Name()), + ObjectKey: objectKey, Size: int64(len(animation.TGS)), + SHA256: append([]byte(nil), animation.SHA256...), MimeType: "application/x-tgsticker", + } + } + return nil + } + if err := materialize(write.Models); err != nil { + return domain.StarGiftCollectibleRevision{}, err + } + if err := materialize(write.Patterns); err != nil { + return domain.StarGiftCollectibleRevision{}, err + } + return s.PublishCollectibleRevision(ctx, write) +} + +func (s *Service) CollectiblePreview(ctx context.Context, giftID int64) (domain.StarGiftUpgradePreview, bool, error) { + if s == nil || s.store == nil || giftID <= 0 { + return domain.StarGiftUpgradePreview{}, false, nil + } + revision, ok, err := s.store.ActiveCollectibleRevision(ctx, giftID) + if err != nil || !ok || !revision.Published { + return domain.StarGiftUpgradePreview{}, false, err + } + return domain.StarGiftUpgradePreview{ + GiftID: giftID, Revision: revision.Revision, UpgradeStars: revision.UpgradeStars, SupplyTotal: revision.SupplyTotal, + Issued: revision.Issued, Models: revision.Models, Patterns: revision.Patterns, Backdrops: revision.Backdrops, + SlugPrefix: revision.SlugPrefix, + }, true, nil +} + +func (s *Service) CollectibleAvailability(ctx context.Context, giftIDs []int64) (map[int64]domain.StarGiftCollectibleAvailability, error) { + if s == nil || s.store == nil || len(giftIDs) == 0 { + return map[int64]domain.StarGiftCollectibleAvailability{}, nil + } + return s.store.CollectibleAvailability(ctx, giftIDs) +} + +func (s *Service) CollectibleAnimationJSON(ctx context.Context, giftID int64, kind domain.StarGiftCollectibleAttributeKind, attributeID int64) ([]byte, bool, error) { + if s == nil || s.store == nil { + return nil, false, nil + } + return s.store.CollectibleAnimationJSON(ctx, giftID, kind, attributeID) +} + +func (s *Service) UniqueBySlug(ctx context.Context, slug string) (domain.UniqueStarGift, bool, error) { + if s == nil || s.store == nil { + return domain.UniqueStarGift{}, false, nil + } + return s.store.UniqueBySlug(ctx, slug) +} + +func (s *Service) UniqueByID(ctx context.Context, uniqueGiftID int64) (domain.UniqueStarGift, bool, error) { + if s == nil || s.store == nil { + return domain.UniqueStarGift{}, false, nil + } + return s.store.UniqueByID(ctx, uniqueGiftID) +} + +func (s *Service) UniqueByIDs(ctx context.Context, uniqueGiftIDs []int64) (map[int64]domain.UniqueStarGift, error) { + if s == nil || s.store == nil || len(uniqueGiftIDs) == 0 { + return map[int64]domain.UniqueStarGift{}, nil + } + return s.store.UniqueByIDs(ctx, uniqueGiftIDs) +} + +func (s *Service) Upgrade(ctx context.Context, req domain.StarGiftUpgradeRequest) (domain.StarGiftUpgradeResult, error) { + if s == nil || s.upgrades == nil { + return domain.StarGiftUpgradeResult{}, fmt.Errorf("star gift upgrade store is not configured") + } + result, err := s.upgrades.UpgradeStarGift(ctx, req) + if err == nil { + s.InvalidateStarGiftCatalog() + } + return result, err +} + +func (s *Service) ListCollections(ctx context.Context, owner domain.Peer) ([]domain.StarGiftCollection, error) { + return s.store.ListCollections(ctx, owner) +} + +func (s *Service) CreateCollection(ctx context.Context, owner domain.Peer, title string, savedGiftIDs []int64) (domain.StarGiftCollection, error) { + return s.store.CreateCollection(ctx, owner, title, savedGiftIDs) +} + +func (s *Service) UpdateCollection(ctx context.Context, owner domain.Peer, collectionID int, patch domain.StarGiftCollectionPatch) (domain.StarGiftCollection, error) { + return s.store.UpdateCollection(ctx, owner, collectionID, patch) +} + +func (s *Service) DeleteCollection(ctx context.Context, owner domain.Peer, collectionID int) (bool, error) { + return s.store.DeleteCollection(ctx, owner, collectionID) +} + +func (s *Service) ReorderCollections(ctx context.Context, owner domain.Peer, collectionIDs []int) error { + return s.store.ReorderCollections(ctx, owner, collectionIDs) +} + +func (s *Service) SetPinned(ctx context.Context, owner domain.Peer, savedGiftIDs []int64) error { + return s.store.SetPinned(ctx, owner, savedGiftIDs) } -// RecordSavedGift 持久化一条收到的礼物实例,返回行 id。 func (s *Service) RecordSavedGift(ctx context.Context, gift domain.SavedStarGift) (int64, error) { return s.store.Create(ctx, gift) } -// ListSaved 分页返回某 owner 收到的礼物。 func (s *Service) ListSaved(ctx context.Context, owner domain.Peer, excludeUnsaved bool, offset string, limit int) (domain.SavedStarGiftPage, error) { - if len(offset) > domain.MaxStarGiftsOffsetBytes { - offset = "" - } - if limit <= 0 || limit > domain.MaxSavedStarGiftsLimit { - limit = domain.MaxSavedStarGiftsLimit - } - return s.store.ListByOwner(ctx, owner, excludeUnsaved, offset, limit) + return s.ListSavedFiltered(ctx, domain.SavedStarGiftFilter{ + Owner: owner, ExcludeUnsaved: excludeUnsaved, Offset: offset, Limit: limit, + }) +} + +func (s *Service) ListSavedFiltered(ctx context.Context, filter domain.SavedStarGiftFilter) (domain.SavedStarGiftPage, error) { + offset := filter.Offset + if len(offset) > domain.MaxStarGiftsOffsetBytes { + filter.Offset = "" + } + if filter.Limit <= 0 || filter.Limit > domain.MaxSavedStarGiftsLimit { + filter.Limit = domain.MaxSavedStarGiftsLimit + } + return s.store.ListByOwnerFiltered(ctx, filter) } -// GetSaved 按协议引用取礼物实例。 func (s *Service) GetSaved(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, bool, error) { return s.store.GetByRef(ctx, ref) } -// CountSaved 返回某 owner 展示在资料的礼物数(full.stargifts_count)。 +func (s *Service) ResolveSavedIDs(ctx context.Context, owner domain.Peer, refs []domain.SavedStarGiftRef) ([]int64, error) { + return s.store.ResolveSavedIDs(ctx, owner, refs) +} + func (s *Service) CountSaved(ctx context.Context, owner domain.Peer) (int, error) { return s.store.CountByOwner(ctx, owner) } -// ToggleSaved 切换礼物在资料的展示(saveStarGift)。 func (s *Service) ToggleSaved(ctx context.Context, ref domain.SavedStarGiftRef, unsaved bool) (bool, error) { return s.store.SetUnsaved(ctx, ref, unsaved) } -// Convert 把礼物标记为已转换(convertStarGift),返回该行供调用方据 ConvertStars 入账。 func (s *Service) Convert(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, error) { return s.store.MarkConverted(ctx, ref) } + +func randomPositiveInt64() (int64, error) { + var raw [8]byte + if _, err := rand.Read(raw[:]); err != nil { + return 0, fmt.Errorf("generate star gift id: %w", err) + } + id := int64(binary.LittleEndian.Uint64(raw[:]) & 0x7fffffffffffffff) + if id == 0 { + id = 1 + } + return id, nil +} diff --git a/internal/app/stargifts/service_test.go b/internal/app/stargifts/service_test.go index 0fb4ac87..8c8d4b42 100644 --- a/internal/app/stargifts/service_test.go +++ b/internal/app/stargifts/service_test.go @@ -9,40 +9,28 @@ import ( "telesrv/internal/store/memory" ) -type fakeCatalog struct { - gifts []domain.StarGift - calls int -} - -func (f *fakeCatalog) BuildStarGiftCatalog(_ context.Context) ([]domain.StarGift, error) { - f.calls++ - return f.gifts, nil -} - -func newTestService(gifts []domain.StarGift) (*Service, *fakeCatalog) { - cat := &fakeCatalog{gifts: gifts} - return NewService(memory.NewStarGiftStore(), cat), cat +func newTestService(gifts []domain.StarGift) (*Service, *memory.StarGiftStore) { + st := memory.NewStarGiftStore() + st.SeedCatalog(gifts) + return NewService(st, nil, 2), st } func TestCatalogCachedAndHash(t *testing.T) { gifts := []domain.StarGift{ - {ID: 1, Stars: 15, ConvertStars: 15, Title: "Heart"}, - {ID: 2, Stars: 50, ConvertStars: 50, Title: "Cake"}, + {ID: 1, RevisionID: 11, Stars: 15, ConvertStars: 15, Title: "Heart"}, + {ID: 2, RevisionID: 12, Stars: 50, ConvertStars: 50, Title: "Cake"}, } - svc, cat := newTestService(gifts) + svc, _ := newTestService(gifts) ctx := context.Background() got, err := svc.Catalog(ctx) if err != nil || len(got) != 2 { t.Fatalf("catalog = %d err %v, want 2", len(got), err) } - // 再取一次不重新构建(缓存)。 + // 再取一次命中进程内目录缓存。 if _, err := svc.Catalog(ctx); err != nil { t.Fatalf("catalog#2: %v", err) } - if cat.calls != 1 { - t.Fatalf("BuildStarGiftCatalog called %d times, want 1 (cached)", cat.calls) - } hash, err := svc.CatalogHash(ctx) if err != nil || hash != domain.StarGiftCatalogHash(gifts) { t.Fatalf("hash = %d err %v, want %d", hash, err, domain.StarGiftCatalogHash(gifts)) @@ -61,11 +49,15 @@ func TestSavedGiftLifecycle(t *testing.T) { owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001} id, err := svc.RecordSavedGift(ctx, domain.SavedStarGift{ - Owner: owner, FromUserID: 2002, GiftID: 1, MsgID: 50, Date: 1700000000, ConvertStars: 15, + Owner: owner, FromUserID: 2002, GiftID: 1, RevisionID: 11, MsgID: 50, Date: 1700000000, ConvertStars: 15, }) if err != nil || id == 0 { t.Fatalf("RecordSavedGift = %d err %v", id, err) } + collection, err := svc.CreateCollection(ctx, owner, "Inbox", []int64{id}) + if err != nil || len(collection.GiftIDs) != 1 { + t.Fatalf("CreateCollection = %+v err %v", collection, err) + } page, err := svc.ListSaved(ctx, owner, false, "", 100) if err != nil || len(page.Gifts) != 1 || page.Count != 1 { @@ -99,6 +91,11 @@ func TestSavedGiftLifecycle(t *testing.T) { if len(after.Gifts) != 0 { t.Fatalf("list after convert = %d, want 0", len(after.Gifts)) } + collections, err := svc.ListCollections(ctx, owner) + if err != nil || len(collections) != 1 || len(collections[0].GiftIDs) != 0 || + collections[0].Hash != domain.StarGiftCollectionHash("Inbox", nil) { + t.Fatalf("collection after convert = %+v err %v, want empty membership and refreshed hash", collections, err) + } // 重复转换被拒。 if _, err := svc.Convert(ctx, ref); !errors.Is(err, domain.ErrStarGiftAlreadyConverted) { t.Fatalf("double convert err = %v, want ErrStarGiftAlreadyConverted", err) @@ -111,7 +108,7 @@ func TestChannelSavedGiftAllocatesSavedIDWithoutMessage(t *testing.T) { owner := domain.Peer{Type: domain.PeerTypeChannel, ID: 2001} savedID, err := svc.RecordSavedGift(ctx, domain.SavedStarGift{ - Owner: owner, FromUserID: 1001, GiftID: 1, MsgID: 0, SavedID: 0, + Owner: owner, FromUserID: 1001, GiftID: 1, RevisionID: 11, MsgID: 0, SavedID: 0, Date: 1700000000, ConvertStars: 15, }) if err != nil || savedID == 0 { @@ -133,7 +130,7 @@ func TestSavedGiftPagination(t *testing.T) { owner := domain.Peer{Type: domain.PeerTypeUser, ID: 1001} for i := 0; i < 5; i++ { if _, err := svc.RecordSavedGift(ctx, domain.SavedStarGift{ - Owner: owner, FromUserID: 2002, GiftID: 1, MsgID: 100 + i, Date: 1700000000 + i, ConvertStars: 15, + Owner: owner, FromUserID: 2002, GiftID: 1, RevisionID: 11, MsgID: 100 + i, Date: 1700000000 + i, ConvertStars: 15, }); err != nil { t.Fatalf("record#%d: %v", i, err) } diff --git a/internal/domain/media.go b/internal/domain/media.go index a042163e..1df60ebf 100644 --- a/internal/domain/media.go +++ b/internal/domain/media.go @@ -561,6 +561,10 @@ const ( // MessageServiceActionStarGift 映射 messageActionStarGift:收到一份 Star 礼物。 // 礼物快照(贴纸/星价)内嵌在 action 里,收礼人无需额外拉取即可渲染气泡。 MessageServiceActionStarGift MessageServiceActionKind = "star_gift" + // MessageServiceActionStarGiftUnique maps messageActionStarGiftUnique. The + // immutable collectible snapshot is carried by the service message so an + // exact replay/difference never depends on mutable catalog state. + MessageServiceActionStarGiftUnique MessageServiceActionKind = "star_gift_unique" ) // MessagePhoneCallAction 是 messageActionPhoneCall 的协议中立载荷。 @@ -617,24 +621,42 @@ type MessageServiceAction struct { RequestedPeer *MessageRequestedPeerAction `json:"requested_peer,omitempty"` ChatThemeEmoticon string `json:"chat_theme_emoticon,omitempty"` StarGift *MessageStarGiftAction `json:"star_gift,omitempty"` + StarGiftUnique *MessageStarGiftUniqueAction `json:"star_gift_unique,omitempty"` } // MessageStarGiftAction 是 messageActionStarGift 的协议中立载荷:内嵌礼物快照(贴纸/星价) // 使收礼人无需额外拉取即可渲染。PeerUserID/PeerChannelID 为收礼方;NameHidden 时下发不暴露 from。 type MessageStarGiftAction struct { - GiftID int64 `json:"gift_id"` - Stars int64 `json:"stars"` - ConvertStars int64 `json:"convert_stars,omitempty"` - Title string `json:"title,omitempty"` - Sticker *Document `json:"sticker,omitempty"` - Message string `json:"message,omitempty"` - FromUserID int64 `json:"from_user_id,omitempty"` - PeerUserID int64 `json:"peer_user_id,omitempty"` - PeerChannelID int64 `json:"peer_channel_id,omitempty"` - SavedID int64 `json:"saved_id,omitempty"` - NameHidden bool `json:"name_hidden,omitempty"` - Saved bool `json:"saved,omitempty"` - Converted bool `json:"converted,omitempty"` + GiftID int64 `json:"gift_id"` + Stars int64 `json:"stars"` + ConvertStars int64 `json:"convert_stars,omitempty"` + Title string `json:"title,omitempty"` + Sticker *Document `json:"sticker,omitempty"` + Message string `json:"message,omitempty"` + FromUserID int64 `json:"from_user_id,omitempty"` + PeerUserID int64 `json:"peer_user_id,omitempty"` + PeerChannelID int64 `json:"peer_channel_id,omitempty"` + SavedID int64 `json:"saved_id,omitempty"` + NameHidden bool `json:"name_hidden,omitempty"` + Saved bool `json:"saved,omitempty"` + Converted bool `json:"converted,omitempty"` + CanUpgrade bool `json:"can_upgrade,omitempty"` + PrepaidUpgrade bool `json:"prepaid_upgrade,omitempty"` + UpgradeStars int64 `json:"upgrade_stars,omitempty"` + UpgradeMsgID int `json:"upgrade_msg_id,omitempty"` +} + +// MessageStarGiftUniqueAction is the protocol-neutral payload of an upgrade +// service message. Commercial transfer/resale/export fields are intentionally +// absent from the collectibles mainline. +type MessageStarGiftUniqueAction struct { + Gift UniqueStarGift `json:"gift"` + FromUserID int64 `json:"from_user_id,omitempty"` + Peer Peer `json:"peer"` + SavedID int64 `json:"saved_id,omitempty"` + Upgrade bool `json:"upgrade,omitempty"` + Saved bool `json:"saved,omitempty"` + PrepaidUpgrade bool `json:"prepaid_upgrade,omitempty"` } // MessageMedia 是一条消息媒体载荷的业务表示(落库为消息行上的 JSONB 快照)。 diff --git a/internal/domain/message.go b/internal/domain/message.go index 443a224a..5c74ad08 100644 --- a/internal/domain/message.go +++ b/internal/domain/message.go @@ -246,19 +246,23 @@ type MessageFilter struct { // SendPrivateTextRequest 是私聊文本/媒体发送命令。 type SendPrivateTextRequest struct { - SenderUserID int64 - RecipientUserID int64 - RandomID int64 - Message string - Entities []MessageEntity - Media *MessageMedia - Silent bool - NoForwards bool - ReplyTo *MessageReply - Forward *MessageForward - Date int - OriginAuthKeyID [8]byte - OriginSessionID int64 + SenderUserID int64 + RecipientUserID int64 + RandomID int64 + Message string + Entities []MessageEntity + Media *MessageMedia + Silent bool + NoForwards bool + ReplyTo *MessageReply + Forward *MessageForward + Date int + OriginAuthKeyID [8]byte + OriginSessionID int64 + // OriginUserID identifies the authenticated initiator when a server-generated + // service message is authored by another user. Zero preserves the ordinary + // send path where the sender is the initiator. + OriginUserID int64 RecipientBlocked bool // IdempotencyFingerprint 是调用边界对原始、不可变发送请求计算的 SHA-256。 // RPC 层应优先填入原始 TL 请求指纹,避免链接预览、骰子结果、上传媒体 diff --git a/internal/domain/star_gift.go b/internal/domain/star_gift.go index 7df41c3b..3237d03f 100644 --- a/internal/domain/star_gift.go +++ b/internal/domain/star_gift.go @@ -3,36 +3,255 @@ package domain import ( "encoding/base64" "errors" + "regexp" "strconv" + "strings" + "time" ) -// Star gift(payments.sendStarsForm + inputInvoiceStarGift)领域模型。目录是从已 seed 的 -// animated_emoji 合成的静态集合(StarGift);peer 收到的礼物实例落 peer_star_gifts(SavedStarGift)。 +// Star gift(payments.sendStarsForm + inputInvoiceStarGift)领域模型。目录和不可变版本 +// 持久化在 star_gift_catalog(_revisions);peer 收到的礼物实例落 peer_star_gifts。 // 与 Stars 账本配合:发礼 Debit、转换回 Stars 时 Credit。 -// StarGift 是一个可购买礼物目录项(合成、非用户持有)。 +// StarGift 是一个可购买礼物目录项。RevisionID 标识不可变的标题/价格/动画快照。 type StarGift struct { - ID int64 - Stars int64 // 购买价(Stars) - ConvertStars int64 // 收礼人可转换回的 Stars(v1 = Stars,全额) - Title string // 可选标题 - Sticker Document // 礼物贴纸快照(tg 投影必须是带 sticker 属性的有效 Document,否则客户端丢弃) + ID int64 + RevisionID int64 + Stars int64 // 购买价(Stars) + ConvertStars int64 // 收礼人可转换回的 Stars + UpgradeStars int64 // 升级为唯一礼物所需 Stars;0 表示当前不可升级 + UpgradeTotal int // 当前已发布属性池允许发行的唯一礼物总量 + UpgradeIssued int // 当前已发行数量 + Title string // 可选标题 + Sticker Document // 礼物贴纸快照(tg 投影必须是带 sticker 属性的有效 Document,否则客户端丢弃) } // SavedStarGift 是一条已收到的礼物实例(peer_star_gifts 一行)。 type SavedStarGift struct { + ID int64 + Owner Peer // 收礼 peer(user/channel) + FromUserID int64 // 送礼人(匿名也保留真实值供账本,下发时按 NameHidden 决定是否暴露) + GiftID int64 // → StarGift.ID + RevisionID int64 // → star_gift_catalog_revisions.id,历史查询必须按此版本投影 + MsgID int // 用户礼物的私聊 msg_id;频道礼物不进历史,固定为 0 + SavedID int64 // 频道礼物 inputSavedStarGiftChat.saved_id;用户礼物为 0 + Date int // 收到时刻 Unix 秒 + NameHidden bool // 送礼人请求隐藏姓名 + Unsaved bool // 未展示在个人资料(saveStarGift 切换) + Converted bool // 已转换回 Stars(终态,从列表排除) + ConvertStars int64 // 转换可退回的 Stars + PrepaidUpgradeStars int64 // 送礼人随礼物预付的唯一礼物升级额 + Message string // 附言(可选) + UniqueGiftID int64 // 非 0 表示已升级为唯一礼物;与 Converted 互斥 + UpgradeMsgID int // messageActionStarGiftUnique 的 owner 侧消息 id + PinnedOrder int // >0 表示资料页置顶顺序 + CollectionIDs []int // 当前所属集合;按集合顺序稳定返回 + Unique *UniqueStarGift +} + +// StarGiftCollectibleAttributeKind 是唯一礼物三个必选属性槽位。 +type StarGiftCollectibleAttributeKind string + +const ( + StarGiftCollectibleModel StarGiftCollectibleAttributeKind = "model" + StarGiftCollectiblePattern StarGiftCollectibleAttributeKind = "pattern" + StarGiftCollectibleBackdrop StarGiftCollectibleAttributeKind = "backdrop" +) + +// StarGiftCollectibleAttribute 是已发布属性池的一项。RarityPermille 同时是客户端展示的 +// 精确稀有度和升级抽取概率;同一 revision、同一 kind 的总和必须恰好为 1000。 +type StarGiftCollectibleAttribute struct { + ID int64 + CollectibleRevisionID int64 + Kind StarGiftCollectibleAttributeKind + Name string + Document *Document + BackdropID int + CenterColor int + EdgeColor int + PatternColor int + TextColor int + RarityPermille int + SortOrder int + Animation *StarGiftAnimation + Blob *FileBlob +} + +// StarGiftCollectibleRevision 是某普通礼物的一份不可变、可发布属性池。 +type StarGiftCollectibleRevision struct { ID int64 - Owner Peer // 收礼 peer(user/channel) - FromUserID int64 // 送礼人(匿名也保留真实值供账本,下发时按 NameHidden 决定是否暴露) - GiftID int64 // → StarGift.ID - MsgID int // 用户礼物的私聊 msg_id;频道礼物不进历史,固定为 0 - SavedID int64 // 频道礼物 inputSavedStarGiftChat.saved_id;用户礼物为 0 - Date int // 收到时刻 Unix 秒 - NameHidden bool // 送礼人请求隐藏姓名 - Unsaved bool // 未展示在个人资料(saveStarGift 切换) - Converted bool // 已转换回 Stars(终态,从列表排除) - ConvertStars int64 // 转换可退回的 Stars - Message string // 附言(可选) + GiftID int64 + Revision int + UpgradeStars int64 + SupplyTotal int + Issued int + SlugPrefix string + Published bool + Models []StarGiftCollectibleAttribute + Patterns []StarGiftCollectibleAttribute + Backdrops []StarGiftCollectibleAttribute + CreatedBy string + CreatedAt time.Time + PublishedAt time.Time +} + +// StarGiftCollectibleWrite 是后台创建/发布属性池的协议无关输入。 +type StarGiftCollectibleWrite struct { + GiftID int64 + UpgradeStars int64 + SupplyTotal int + SlugPrefix string + Models []StarGiftCollectibleAttribute + Patterns []StarGiftCollectibleAttribute + Backdrops []StarGiftCollectibleAttribute + Actor string + CommandID string +} + +// UniqueStarGift 是一份已经发行的唯一礼物。属性、编号与 slug 一经创建永久不变。 +type UniqueStarGift struct { + ID int64 + GiftID int64 + CollectibleRevisionID int64 + SourceSavedGiftID int64 + Title string + Slug string + Num int + Owner Peer + Model StarGiftCollectibleAttribute + Pattern StarGiftCollectibleAttribute + Backdrop StarGiftCollectibleAttribute + AvailabilityIssued int + AvailabilityTotal int + KeepOriginalDetails bool + OriginalFromUserID int64 + OriginalOwner Peer + OriginalDate int + OriginalMessage string + OriginalNameHidden bool + CreatedAt time.Time +} + +// StarGiftUpgradePreview 是客户端升级弹窗所需的当前价格和属性样例。 +type StarGiftUpgradePreview struct { + GiftID int64 + Revision int + UpgradeStars int64 + SupplyTotal int + Issued int + SlugPrefix string + Models []StarGiftCollectibleAttribute + Patterns []StarGiftCollectibleAttribute + Backdrops []StarGiftCollectibleAttribute +} + +// StarGiftCollectibleAvailability is the lightweight current-pool projection used +// when rendering historical saved gifts. The saved gift keeps its immutable catalog +// revision for appearance and prices, while upgrade availability follows the pool +// currently published for the logical gift ID. +type StarGiftCollectibleAvailability struct { + UpgradeStars int64 + SupplyTotal int + Issued int +} + +// StarGiftUpgradeRequest is one idempotent user-owned upgrade command. Paid +// invoice upgrades set ChargeStars; the direct payments.upgradeStarGift path +// sets RequirePrepaid and charges zero at upgrade time. +type StarGiftUpgradeRequest struct { + UserID int64 + Ref SavedStarGiftRef + KeepOriginalDetails bool + ChargeStars int64 + RequirePrepaid bool + FormID int64 + CommandKey string + Date int + OriginAuthKeyID [8]byte + OriginSessionID int64 +} + +type StarGiftUpgradeResult struct { + Saved SavedStarGift + Unique UniqueStarGift + Balance StarsBalance + Send SendPrivateTextResult + Duplicate bool +} + +// StarGiftCollection 是 peer 资料页中的礼物集合;一份礼物可属于多个集合。 +type StarGiftCollection struct { + Owner Peer + CollectionID int + Title string + GiftIDs []int64 // peer_star_gifts.id,按集合内顺序 + Hash int64 + SortOrder int + CreatedAt time.Time + UpdatedAt time.Time +} + +// StarGiftCollectionPatch 描述 updateStarGiftCollection 的局部更新。 +type StarGiftCollectionPatch struct { + Title *string + DeleteIDs []int64 + AddIDs []int64 + Order []int64 +} + +// StarGiftAnimationFormat 是后台导入源格式。服务端最终总是存储规范化 TGS。 +type StarGiftAnimationFormat string + +const ( + StarGiftAnimationTGS StarGiftAnimationFormat = "tgs" + StarGiftAnimationLottie StarGiftAnimationFormat = "lottie" +) + +// StarGiftAnimation 是已规范化并验证的动画。JSON 用于后台播放,TGS 用于客户端。 +type StarGiftAnimation struct { + SourceName string + SourceFormat StarGiftAnimationFormat + JSON []byte + TGS []byte + SHA256 []byte + Width int + Height int + FrameRate float64 + InPoint float64 + OutPoint float64 +} + +// StarGiftCatalogWrite 是 store 原子创建目录版本所需的协议无关数据。 +type StarGiftCatalogWrite struct { + GiftID int64 // 0 创建新礼物;非 0 为该礼物创建新 revision + Title string + Stars int64 + ConvertStars int64 + Enabled bool + SortOrder int + Document Document + Blob FileBlob + Animation StarGiftAnimation + Actor string + CommandID string +} + +// StarGiftCatalogEntry 是管理后台目录视图。 +type StarGiftCatalogEntry struct { + Gift StarGift + Enabled bool + SortOrder int + Revision int + SourceName string + SourceFormat StarGiftAnimationFormat + AnimationSHA []byte + AnimationSize int64 + Width int + Height int + FrameRate float64 + ReceivedCount int64 + CreatedBy string + UpdatedAt time.Time } // SavedStarGiftRef 是 payments.getSavedStarGift/saveStarGift/convertStarGift 的协议中立引用。 @@ -62,6 +281,24 @@ type SavedStarGiftPage struct { Count int // 总数(未转换、按 excludeUnsaved 过滤后) } +// SavedStarGiftFilter describes the client-visible filters supported by +// payments.getSavedStarGifts. CollectionID is the collection membership filter; +// zero means all collections. The current catalog is used only to decide whether +// a regular gift remains upgradable, while its rendered gift snapshot still comes +// from RevisionID. +type SavedStarGiftFilter struct { + Owner Peer + ExcludeUnsaved bool + ExcludeSaved bool + ExcludeUnlimited bool + ExcludeUnique bool + ExcludeUpgradable bool + ExcludeUnupgradable bool + CollectionID int + Offset string + Limit int +} + // Star gift 边界常量。 const ( // MaxSavedStarGiftsLimit 是 getSavedStarGifts 单页上限。 @@ -70,6 +307,20 @@ const ( MaxStarGiftMessageRunes = 255 // MaxStarGiftsOffsetBytes 是 keyset 游标字符串长度上限。 MaxStarGiftsOffsetBytes = 64 + // MaxStarGiftTGSBytes 限制后台导入的压缩动画,避免管理面上传成为容量旁路。 + MaxStarGiftTGSBytes int64 = 512 << 10 + // MaxStarGiftLottieBytes 限制解压后的 Lottie JSON。 + MaxStarGiftLottieBytes int64 = 4 << 20 + // MaxStarGiftAnimationFrameRate / Seconds 限制管理后台播放器和客户端动画时间轴。 + MaxStarGiftAnimationFrameRate = 120 + MaxStarGiftAnimationSeconds = 30 + // MaxStarGiftCatalogSize 是当前普通礼物目录的有界上限。 + MaxStarGiftCatalogSize = 500 + MaxStarGiftTitleRunes = 128 + MaxStarGiftCollectibleAttributesPerKind = 256 + MaxStarGiftCollectionTitleRunes = 12 + MaxStarGiftCollectionsPerPeer = 100 + MaxStarGiftCollectionItems = 1000 ) // Star gift 哨兵错误(rpc 层 errors.Is 映射为 tgerr)。 @@ -79,20 +330,138 @@ var ( // ErrStarGiftNotFound 表示找不到该已收到礼物实例。 ErrStarGiftNotFound = errors.New("stargift: saved gift not found") // ErrStarGiftAlreadyConverted 表示礼物已转换回 Stars(不可重复转换)。 - ErrStarGiftAlreadyConverted = errors.New("stargift: already converted") + ErrStarGiftAlreadyConverted = errors.New("stargift: already converted") + ErrStarGiftFileInvalid = errors.New("stargift: invalid animation file") + ErrStarGiftCatalogFull = errors.New("stargift: catalog full") + ErrStarGiftCollectibleUnavailable = errors.New("stargift: collectible upgrade unavailable") + ErrStarGiftAlreadyUpgraded = errors.New("stargift: already upgraded") + ErrStarGiftCollectibleSoldOut = errors.New("stargift: collectible supply exhausted") + ErrStarGiftCollectibleInvalid = errors.New("stargift: invalid collectible definition") + ErrStarGiftCollectionNotFound = errors.New("stargift: collection not found") + ErrStarGiftCollectionsFull = errors.New("stargift: collections full") ) -// StarGiftCatalogHash 由目录的 (gift_id, stars) 折叠出稳定 hash,供 getStarGifts NotModified。 +var starGiftCollectibleSlugPrefix = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,47}$`) + +// ValidateStarGiftCollectibleDraft validates the operator-authored definition before animation +// blobs/documents are allocated. This is the validation boundary used by admin dry-runs. +func ValidateStarGiftCollectibleDraft(write StarGiftCollectibleWrite) error { + write.SlugPrefix = strings.TrimSpace(strings.ToLower(write.SlugPrefix)) + if write.GiftID <= 0 || write.UpgradeStars <= 0 || write.SupplyTotal <= 0 || + !starGiftCollectibleSlugPrefix.MatchString(write.SlugPrefix) || strings.TrimSpace(write.CommandID) == "" { + return ErrStarGiftCollectibleInvalid + } + if err := validateStarGiftAttributes(write.Models, StarGiftCollectibleModel, false); err != nil { + return err + } + if err := validateStarGiftAttributes(write.Patterns, StarGiftCollectiblePattern, false); err != nil { + return err + } + return validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, false) +} + +// ValidateStarGiftCollectibleWrite validates a complete publish command. Published pools are +// immutable, so partial definitions are rejected before any document/blob rows are written. +func ValidateStarGiftCollectibleWrite(write StarGiftCollectibleWrite) error { + if err := ValidateStarGiftCollectibleDraft(write); err != nil { + return err + } + if err := validateStarGiftAttributes(write.Models, StarGiftCollectibleModel, true); err != nil { + return err + } + if err := validateStarGiftAttributes(write.Patterns, StarGiftCollectiblePattern, true); err != nil { + return err + } + return validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, true) +} + +func validateStarGiftAttributes(attributes []StarGiftCollectibleAttribute, kind StarGiftCollectibleAttributeKind, requireStoredAsset bool) error { + if len(attributes) == 0 || len(attributes) > MaxStarGiftCollectibleAttributesPerKind { + return ErrStarGiftCollectibleInvalid + } + seen := make(map[string]struct{}, len(attributes)) + total := 0 + for _, attribute := range attributes { + name := strings.TrimSpace(attribute.Name) + if attribute.Kind != kind || name == "" || len([]rune(name)) > MaxStarGiftTitleRunes || + attribute.RarityPermille <= 0 || attribute.RarityPermille > 1000 { + return ErrStarGiftCollectibleInvalid + } + key := strings.ToLower(name) + if _, ok := seen[key]; ok { + return ErrStarGiftCollectibleInvalid + } + seen[key] = struct{}{} + total += attribute.RarityPermille + switch kind { + case StarGiftCollectibleModel, StarGiftCollectiblePattern: + if attribute.Animation == nil || len(attribute.Animation.JSON) == 0 || + len(attribute.Animation.TGS) == 0 || len(attribute.Animation.SHA256) != 32 { + return ErrStarGiftCollectibleInvalid + } + if requireStoredAsset && (attribute.Document == nil || !attribute.Document.IsSticker() || + attribute.Document.MimeType != "application/x-tgsticker" || attribute.Blob == nil) { + return ErrStarGiftCollectibleInvalid + } + case StarGiftCollectibleBackdrop: + if attribute.BackdropID <= 0 || attribute.Document != nil || + attribute.CenterColor < 0 || attribute.CenterColor > 0xffffff || + attribute.EdgeColor < 0 || attribute.EdgeColor > 0xffffff || + attribute.PatternColor < 0 || attribute.PatternColor > 0xffffff || + attribute.TextColor < 0 || attribute.TextColor > 0xffffff { + return ErrStarGiftCollectibleInvalid + } + default: + return ErrStarGiftCollectibleInvalid + } + } + if total != 1000 { + return ErrStarGiftCollectibleInvalid + } + return nil +} + +// StarGiftCatalogHash 由客户端可见目录字段折叠出稳定 hash,供 getStarGifts NotModified。 func StarGiftCatalogHash(catalog []StarGift) int { var h uint64 for _, g := range catalog { h ^= uint64(g.ID) h = h*0x4f25 + uint64(g.ID) + h = h*0x4f25 + uint64(g.RevisionID) h = h*0x4f25 + uint64(g.Stars) + h = h*0x4f25 + uint64(g.ConvertStars) + h = h*0x4f25 + uint64(g.UpgradeStars) + h = h*0x4f25 + uint64(g.UpgradeTotal) + h = h*0x4f25 + uint64(g.UpgradeIssued) + h = h*0x4f25 + uint64(g.Sticker.ID) + for _, r := range g.Title { + h = h*131 + uint64(r) + } } return int(h & 0x7fffffff) } +// StarGiftCollectionsHash 按服务端返回顺序折叠每个集合自己的稳定 hash。 +func StarGiftCollectionsHash(collections []StarGiftCollection) int64 { + var h uint64 + for _, collection := range collections { + h = h*0x4f25 + uint64(collection.Hash) + } + return int64(h & 0x7fffffffffffffff) +} + +// StarGiftCollectionHash returns the per-collection hash exposed by starGiftCollection.hash. +func StarGiftCollectionHash(title string, giftIDs []int64) int64 { + h := uint64(0x534743) + for _, r := range title { + h = h*131 + uint64(r) + } + for _, id := range giftIDs { + h = h*0x4f25 + uint64(id) + } + return int64(h & 0x7fffffffffffffff) +} + // EncodeStarGiftCursor / DecodeStarGiftCursor 是 saved gifts keyset 游标(最后一条实例 id)。 func EncodeStarGiftCursor(id int64) string { return base64.RawURLEncoding.EncodeToString([]byte(strconv.FormatInt(id, 10))) diff --git a/internal/domain/stars.go b/internal/domain/stars.go index f0672e52..383a188d 100644 --- a/internal/domain/stars.go +++ b/internal/domain/stars.go @@ -20,21 +20,22 @@ type StarsBalance struct { type StarsTransactionReason string const ( - StarsReasonGrant StarsTransactionReason = "grant" // 起始余额自动授予 - StarsReasonTopup StarsTransactionReason = "topup" // 充值(本地铸造) - StarsReasonReaction StarsTransactionReason = "reaction" // 付费 reaction 花费 - StarsReasonGift StarsTransactionReason = "gift" // 星礼花费/收取 - StarsReasonPaidMedia StarsTransactionReason = "paid_media" // 付费媒体解锁 - StarsReasonAdjust StarsTransactionReason = "adjust" // 兜底/人工调整 + StarsReasonGrant StarsTransactionReason = "grant" // 起始余额自动授予 + StarsReasonTopup StarsTransactionReason = "topup" // 充值(本地铸造) + StarsReasonReaction StarsTransactionReason = "reaction" // 付费 reaction 花费 + StarsReasonGift StarsTransactionReason = "gift" // 星礼花费/收取 + StarsReasonGiftUpgrade StarsTransactionReason = "gift_upgrade" // 普通礼物升级为唯一礼物 + StarsReasonPaidMedia StarsTransactionReason = "paid_media" // 付费媒体解锁 + StarsReasonAdjust StarsTransactionReason = "adjust" // 兜底/人工调整 ) // StarsTransaction 是一条账本流水。amount 带符号:贷记 > 0(含 refund/收取),借记 < 0。 type StarsTransaction struct { - ID int64 // 单调递增账本 id(keyset 游标) - UserID int64 // 账本归属 - Peer Peer // 对手方(grant/topup 等无对手时为零 Peer) - Amount int64 // 带符号金额 - Date int // Unix 秒 + ID int64 // 单调递增账本 id(keyset 游标) + UserID int64 // 账本归属 + Peer Peer // 对手方(grant/topup 等无对手时为零 Peer) + Amount int64 // 带符号金额 + Date int // Unix 秒 Reason StarsTransactionReason Title string // 可选,投影到 tg.StarsTransaction.Title Description string // 可选,投影到 tg.StarsTransaction.Description diff --git a/internal/rpc/convert_messages.go b/internal/rpc/convert_messages.go index 7cb91901..1e119b35 100644 --- a/internal/rpc/convert_messages.go +++ b/internal/rpc/convert_messages.go @@ -220,6 +220,25 @@ func tgMessageServiceAction(msg domain.Message) tg.MessageActionClass { } case domain.MessageServiceActionStarGift: return tgMessageActionStarGift(m.ServiceAction.StarGift) + case domain.MessageServiceActionStarGiftUnique: + action := m.ServiceAction.StarGiftUnique + if action == nil { + return &tg.MessageActionEmpty{} + } + out := &tg.MessageActionStarGiftUnique{ + Upgrade: action.Upgrade, Saved: action.Saved, PrepaidUpgrade: action.PrepaidUpgrade, + Gift: tgUniqueStarGift(action.Gift), + } + if action.FromUserID != 0 { + out.SetFromID(&tg.PeerUser{UserID: action.FromUserID}) + } + if peer := tgPeer(action.Peer); peer != nil { + out.SetPeer(peer) + } + if action.SavedID != 0 { + out.SetSavedID(action.SavedID) + } + return out default: return &tg.MessageActionEmpty{} } diff --git a/internal/rpc/deps.go b/internal/rpc/deps.go index de7d813a..fe21dbbc 100644 --- a/internal/rpc/deps.go +++ b/internal/rpc/deps.go @@ -864,12 +864,27 @@ type GiftsService interface { Catalog(ctx context.Context) ([]domain.StarGift, error) CatalogHash(ctx context.Context) (int, error) GiftByID(ctx context.Context, id int64) (domain.StarGift, bool, error) + GiftRevisionByID(ctx context.Context, revisionID int64) (domain.StarGift, bool, error) + CollectiblePreview(ctx context.Context, giftID int64) (domain.StarGiftUpgradePreview, bool, error) + CollectibleAvailability(ctx context.Context, giftIDs []int64) (map[int64]domain.StarGiftCollectibleAvailability, error) + UniqueBySlug(ctx context.Context, slug string) (domain.UniqueStarGift, bool, error) + UniqueByID(ctx context.Context, uniqueGiftID int64) (domain.UniqueStarGift, bool, error) + UniqueByIDs(ctx context.Context, uniqueGiftIDs []int64) (map[int64]domain.UniqueStarGift, error) + Upgrade(ctx context.Context, req domain.StarGiftUpgradeRequest) (domain.StarGiftUpgradeResult, error) RecordSavedGift(ctx context.Context, gift domain.SavedStarGift) (int64, error) ListSaved(ctx context.Context, owner domain.Peer, excludeUnsaved bool, offset string, limit int) (domain.SavedStarGiftPage, error) + ListSavedFiltered(ctx context.Context, filter domain.SavedStarGiftFilter) (domain.SavedStarGiftPage, error) GetSaved(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, bool, error) + ResolveSavedIDs(ctx context.Context, owner domain.Peer, refs []domain.SavedStarGiftRef) ([]int64, error) CountSaved(ctx context.Context, owner domain.Peer) (int, error) ToggleSaved(ctx context.Context, ref domain.SavedStarGiftRef, unsaved bool) (bool, error) Convert(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, error) + ListCollections(ctx context.Context, owner domain.Peer) ([]domain.StarGiftCollection, error) + CreateCollection(ctx context.Context, owner domain.Peer, title string, savedGiftIDs []int64) (domain.StarGiftCollection, error) + UpdateCollection(ctx context.Context, owner domain.Peer, collectionID int, patch domain.StarGiftCollectionPatch) (domain.StarGiftCollection, error) + DeleteCollection(ctx context.Context, owner domain.Peer, collectionID int) (bool, error) + ReorderCollections(ctx context.Context, owner domain.Peer, collectionIDs []int) error + SetPinned(ctx context.Context, owner domain.Peer, savedGiftIDs []int64) error } // StarsService 抽象 Stars 本地账本(app/stars):余额查询、贷记/借记、流水分页。 diff --git a/internal/rpc/passive_compat_test.go b/internal/rpc/passive_compat_test.go index c329a891..66e7a12b 100644 --- a/internal/rpc/passive_compat_test.go +++ b/internal/rpc/passive_compat_test.go @@ -82,7 +82,7 @@ func TestAccountGetWallPapersReturnsDefaultCatalog(t *testing.T) { t.Fatalf("boxed response type = %T, want *tg.AccountWallPapers", box.WallPapers) } if wallpapers.Hash == 0 || len(wallpapers.Wallpapers) == 0 { - t.Fatalf("wallpapers = %+v, want stable Default catalog", wallpapers) + t.Fatalf("wallpapers = %+v, want stable default catalog", wallpapers) } } @@ -189,7 +189,7 @@ func TestAccountWallpaperSeedLookupAndAckRPCs(t *testing.T) { } } -func TestPaymentsGetStarGiftCollectionsReturnsEmptyAndValidatesPeer(t *testing.T) { +func TestPaymentsGetStarGiftCollectionsNoServiceFallbackAndValidatesPeer(t *testing.T) { r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System) ctx := WithUserID(context.Background(), 1000000001) diff --git a/internal/rpc/payments.go b/internal/rpc/payments.go index 0cd20576..a1cb70b1 100644 --- a/internal/rpc/payments.go +++ b/internal/rpc/payments.go @@ -30,25 +30,21 @@ func (r *Router) registerPayments(d *tg.ServerDispatcher) { return tdesktop.StarGiftActiveAuctions(), nil }) d.OnPaymentsGetStarGifts(r.onPaymentsGetStarGifts) + d.OnPaymentsGetStarGiftUpgradePreview(r.onPaymentsGetStarGiftUpgradePreview) + d.OnPaymentsGetUniqueStarGift(r.onPaymentsGetUniqueStarGift) d.OnPaymentsGetPaymentForm(r.onPaymentsGetPaymentForm) d.OnPaymentsSendStarsForm(r.onPaymentsSendStarsForm) d.OnPaymentsGetSavedStarGifts(r.onPaymentsGetSavedStarGifts) d.OnPaymentsGetSavedStarGift(r.onPaymentsGetSavedStarGift) d.OnPaymentsSaveStarGift(r.onPaymentsSaveStarGift) d.OnPaymentsConvertStarGift(r.onPaymentsConvertStarGift) - d.OnPaymentsGetStarGiftCollections(func(ctx context.Context, req *tg.PaymentsGetStarGiftCollectionsRequest) (tg.PaymentsStarGiftCollectionsClass, error) { - userID, _, err := r.currentUserID(ctx) - if err != nil { - return nil, internalErr() - } - if req == nil { - return nil, peerIDInvalidErr() - } - if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil { - return nil, err - } - return tdesktop.StarGiftCollections(), nil - }) + d.OnPaymentsUpgradeStarGift(r.onPaymentsUpgradeStarGift) + d.OnPaymentsGetStarGiftCollections(r.onPaymentsGetStarGiftCollections) + d.OnPaymentsCreateStarGiftCollection(r.onPaymentsCreateStarGiftCollection) + d.OnPaymentsUpdateStarGiftCollection(r.onPaymentsUpdateStarGiftCollection) + d.OnPaymentsDeleteStarGiftCollection(r.onPaymentsDeleteStarGiftCollection) + d.OnPaymentsReorderStarGiftCollections(r.onPaymentsReorderStarGiftCollections) + d.OnPaymentsToggleStarGiftsPinnedToTop(r.onPaymentsToggleStarGiftsPinnedToTop) d.OnPaymentsGetStarsRevenueAdsAccountURL(func(ctx context.Context, peer tg.InputPeerClass) (*tg.PaymentsStarsRevenueAdsAccountURL, error) { userID, _, err := r.currentUserID(ctx) if err != nil { diff --git a/internal/rpc/payments_star_gift_collections.go b/internal/rpc/payments_star_gift_collections.go new file mode 100644 index 00000000..0259c3b7 --- /dev/null +++ b/internal/rpc/payments_star_gift_collections.go @@ -0,0 +1,235 @@ +package rpc + +import ( + "context" + "errors" + "strings" + + "github.com/iamxvbaba/td/tg" + + "telesrv/internal/domain" +) + +func (r *Router) onPaymentsGetStarGiftCollections(ctx context.Context, req *tg.PaymentsGetStarGiftCollectionsRequest) (tg.PaymentsStarGiftCollectionsClass, error) { + if req == nil { + return nil, inputRequestInvalidErr() + } + userID, _, err := r.currentUserID(ctx) + if err != nil { + return nil, internalErr() + } + owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer) + if err != nil { + return nil, err + } + if r.deps.Gifts == nil { + return &tg.PaymentsStarGiftCollections{Collections: []tg.StarGiftCollection{}}, nil + } + collections, err := r.deps.Gifts.ListCollections(ctx, owner) + if err != nil { + return nil, starGiftCollectionErr(err) + } + if req.Hash != 0 && req.Hash == domain.StarGiftCollectionsHash(collections) { + return &tg.PaymentsStarGiftCollectionsNotModified{}, nil + } + return &tg.PaymentsStarGiftCollections{Collections: tgStarGiftCollections(collections)}, nil +} + +func (r *Router) onPaymentsCreateStarGiftCollection(ctx context.Context, req *tg.PaymentsCreateStarGiftCollectionRequest) (*tg.StarGiftCollection, error) { + if req == nil || r.deps.Gifts == nil { + return nil, inputRequestInvalidErr() + } + userID, _, err := r.currentUserID(ctx) + if err != nil { + return nil, internalErr() + } + owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer) + if err != nil { + return nil, err + } + if err := r.ensureCanManageStarGiftOwner(ctx, userID, owner); err != nil { + return nil, err + } + ids, err := r.resolveStarGiftCollectionRefs(ctx, userID, owner, req.Stargift) + if err != nil { + return nil, err + } + collection, err := r.deps.Gifts.CreateCollection(ctx, owner, strings.TrimSpace(req.Title), ids) + if err != nil { + return nil, starGiftCollectionErr(err) + } + r.invalidateStarGiftOwnerProjection(owner) + out := tgStarGiftCollection(collection) + return &out, nil +} + +func (r *Router) onPaymentsUpdateStarGiftCollection(ctx context.Context, req *tg.PaymentsUpdateStarGiftCollectionRequest) (*tg.StarGiftCollection, error) { + if req == nil || req.CollectionID <= 0 || r.deps.Gifts == nil { + return nil, inputRequestInvalidErr() + } + userID, _, err := r.currentUserID(ctx) + if err != nil { + return nil, internalErr() + } + owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer) + if err != nil { + return nil, err + } + if err := r.ensureCanManageStarGiftOwner(ctx, userID, owner); err != nil { + return nil, err + } + patch := domain.StarGiftCollectionPatch{} + if title, ok := req.GetTitle(); ok { + title = strings.TrimSpace(title) + patch.Title = &title + } + if refs, ok := req.GetDeleteStargift(); ok { + patch.DeleteIDs, err = r.resolveStarGiftCollectionRefs(ctx, userID, owner, refs) + if err != nil { + return nil, err + } + } + if refs, ok := req.GetAddStargift(); ok { + patch.AddIDs, err = r.resolveStarGiftCollectionRefs(ctx, userID, owner, refs) + if err != nil { + return nil, err + } + } + if refs, ok := req.GetOrder(); ok { + patch.Order, err = r.resolveStarGiftCollectionRefs(ctx, userID, owner, refs) + if err != nil { + return nil, err + } + } + collection, err := r.deps.Gifts.UpdateCollection(ctx, owner, req.CollectionID, patch) + if err != nil { + return nil, starGiftCollectionErr(err) + } + r.invalidateStarGiftOwnerProjection(owner) + out := tgStarGiftCollection(collection) + return &out, nil +} + +func (r *Router) onPaymentsDeleteStarGiftCollection(ctx context.Context, req *tg.PaymentsDeleteStarGiftCollectionRequest) (bool, error) { + if req == nil || req.CollectionID <= 0 || r.deps.Gifts == nil { + return false, inputRequestInvalidErr() + } + userID, _, err := r.currentUserID(ctx) + if err != nil { + return false, internalErr() + } + owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer) + if err != nil { + return false, err + } + if err := r.ensureCanManageStarGiftOwner(ctx, userID, owner); err != nil { + return false, err + } + deleted, err := r.deps.Gifts.DeleteCollection(ctx, owner, req.CollectionID) + if err != nil { + return false, starGiftCollectionErr(err) + } + if !deleted { + return false, starGiftCollectionErr(domain.ErrStarGiftCollectionNotFound) + } + r.invalidateStarGiftOwnerProjection(owner) + return true, nil +} + +func (r *Router) onPaymentsReorderStarGiftCollections(ctx context.Context, req *tg.PaymentsReorderStarGiftCollectionsRequest) (bool, error) { + if req == nil || r.deps.Gifts == nil { + return false, inputRequestInvalidErr() + } + userID, _, err := r.currentUserID(ctx) + if err != nil { + return false, internalErr() + } + owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer) + if err != nil { + return false, err + } + if err := r.ensureCanManageStarGiftOwner(ctx, userID, owner); err != nil { + return false, err + } + if err := r.deps.Gifts.ReorderCollections(ctx, owner, req.Order); err != nil { + return false, starGiftCollectionErr(err) + } + return true, nil +} + +func (r *Router) onPaymentsToggleStarGiftsPinnedToTop(ctx context.Context, req *tg.PaymentsToggleStarGiftsPinnedToTopRequest) (bool, error) { + if req == nil || r.deps.Gifts == nil { + return false, inputRequestInvalidErr() + } + userID, _, err := r.currentUserID(ctx) + if err != nil { + return false, internalErr() + } + owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer) + if err != nil { + return false, err + } + if err := r.ensureCanManageStarGiftOwner(ctx, userID, owner); err != nil { + return false, err + } + ids, err := r.resolveStarGiftCollectionRefs(ctx, userID, owner, req.Stargift) + if err != nil { + return false, err + } + if err := r.deps.Gifts.SetPinned(ctx, owner, ids); err != nil { + return false, starGiftCollectionErr(err) + } + r.invalidateStarGiftOwnerProjection(owner) + return true, nil +} + +func (r *Router) resolveStarGiftCollectionRefs(ctx context.Context, userID int64, owner domain.Peer, refs []tg.InputSavedStarGiftClass) ([]int64, error) { + if len(refs) > domain.MaxStarGiftCollectionItems { + return nil, inputRequestInvalidErr() + } + domainRefs := make([]domain.SavedStarGiftRef, 0, len(refs)) + for _, input := range refs { + ref, ok, err := r.starGiftRefFromInput(ctx, userID, input) + if err != nil { + return nil, err + } + if !ok || ref.Owner != owner { + return nil, starGiftInvalidErr() + } + domainRefs = append(domainRefs, ref) + } + ids, err := r.deps.Gifts.ResolveSavedIDs(ctx, owner, domainRefs) + if err != nil { + return nil, starGiftCollectionErr(err) + } + return ids, nil +} + +func tgStarGiftCollections(in []domain.StarGiftCollection) []tg.StarGiftCollection { + out := make([]tg.StarGiftCollection, 0, len(in)) + for _, collection := range in { + out = append(out, tgStarGiftCollection(collection)) + } + return out +} + +func tgStarGiftCollection(in domain.StarGiftCollection) tg.StarGiftCollection { + return tg.StarGiftCollection{ + CollectionID: in.CollectionID, + Title: in.Title, + GiftsCount: len(in.GiftIDs), + Hash: in.Hash, + } +} + +func starGiftCollectionErr(err error) error { + switch { + case errors.Is(err, domain.ErrStarGiftNotFound), + errors.Is(err, domain.ErrStarGiftCollectibleInvalid), + errors.Is(err, domain.ErrStarGiftCollectionNotFound), + errors.Is(err, domain.ErrStarGiftCollectionsFull): + return inputRequestInvalidErr() + default: + return internalErr() + } +} diff --git a/internal/rpc/payments_star_gift_unique.go b/internal/rpc/payments_star_gift_unique.go new file mode 100644 index 00000000..b9b54794 --- /dev/null +++ b/internal/rpc/payments_star_gift_unique.go @@ -0,0 +1,278 @@ +package rpc + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/iamxvbaba/td/tg" + + "telesrv/internal/domain" +) + +func (r *Router) starGiftUpgradePaymentForm(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftUpgrade) (tg.PaymentsPaymentFormClass, error) { + saved, preview, err := r.starGiftUpgradeTarget(ctx, userID, inv.Stargift) + if err != nil { + return nil, err + } + return &tg.PaymentsPaymentFormStarGift{ + FormID: starGiftUpgradeFormID(userID, saved.ID, preview.UpgradeStars, inv.KeepOriginalDetails), + Invoice: tg.Invoice{ + Currency: "XTR", + Prices: []tg.LabeledPrice{{Label: "Star gift upgrade", Amount: preview.UpgradeStars}}, + }, + }, nil +} + +func (r *Router) sendStarGiftUpgradeForm(ctx context.Context, userID, formID int64, inv *tg.InputInvoiceStarGiftUpgrade) (tg.PaymentsPaymentResultClass, error) { + saved, preview, err := r.starGiftUpgradeTarget(ctx, userID, inv.Stargift) + if err != nil { + return nil, err + } + wantFormID := starGiftUpgradeFormID(userID, saved.ID, preview.UpgradeStars, inv.KeepOriginalDetails) + if formID == 0 || formID != wantFormID { + return nil, starsFormAmountMismatchErr() + } + result, err := r.deps.Gifts.Upgrade(ctx, domain.StarGiftUpgradeRequest{ + UserID: userID, Ref: domain.SavedStarGiftRef{Owner: saved.Owner, MsgID: saved.MsgID}, + KeepOriginalDetails: inv.KeepOriginalDetails, ChargeStars: preview.UpgradeStars, + FormID: formID, CommandKey: fmt.Sprintf("paid:%d:%d:%t", saved.ID, formID, inv.KeepOriginalDetails), + Date: int(r.clock.Now().Unix()), OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx), + OriginSessionID: sessionIDOrZero(ctx), + }) + if err != nil { + return nil, starGiftUpgradeErr(err) + } + r.invalidateStarGiftOwnerProjection(saved.Owner) + updates := r.tgStarGiftUpgradeUpdates(ctx, userID, result, true) + return &tg.PaymentsPaymentResult{Updates: updates}, nil +} + +func (r *Router) onPaymentsUpgradeStarGift(ctx context.Context, req *tg.PaymentsUpgradeStarGiftRequest) (tg.UpdatesClass, error) { + if req == nil || r.deps.Gifts == nil { + return nil, inputRequestInvalidErr() + } + userID, _, err := r.currentUserID(ctx) + if err != nil { + return nil, internalErr() + } + saved, _, err := r.starGiftUpgradeTarget(ctx, userID, req.Stargift) + if err != nil { + return nil, err + } + if saved.PrepaidUpgradeStars <= 0 { + return nil, starGiftInvalidErr() + } + result, err := r.deps.Gifts.Upgrade(ctx, domain.StarGiftUpgradeRequest{ + UserID: userID, Ref: domain.SavedStarGiftRef{Owner: saved.Owner, MsgID: saved.MsgID}, + KeepOriginalDetails: req.KeepOriginalDetails, RequirePrepaid: true, + CommandKey: fmt.Sprintf("prepaid:%d:%t", saved.ID, req.KeepOriginalDetails), + Date: int(r.clock.Now().Unix()), OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx), + OriginSessionID: sessionIDOrZero(ctx), + }) + if err != nil { + return nil, starGiftUpgradeErr(err) + } + r.invalidateStarGiftOwnerProjection(saved.Owner) + return r.tgStarGiftUpgradeUpdates(ctx, userID, result, false), nil +} + +func (r *Router) starGiftUpgradeTarget(ctx context.Context, userID int64, input tg.InputSavedStarGiftClass) (domain.SavedStarGift, domain.StarGiftUpgradePreview, error) { + if r.deps.Gifts == nil { + return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, notImplementedErr() + } + ref, ok, err := r.starGiftRefFromInput(ctx, userID, input) + if err != nil { + return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, err + } + if !ok || ref.Owner.Type != domain.PeerTypeUser || ref.Owner.ID != userID { + // Channel gift upgrades require a channel pts aggregate and are not silently + // routed through the private-message transaction. + return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, starGiftInvalidErr() + } + saved, found, err := r.deps.Gifts.GetSaved(ctx, ref) + if err != nil { + return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, internalErr() + } + if !found || saved.Converted || saved.UniqueGiftID != 0 { + return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, starGiftInvalidErr() + } + preview, found, err := r.deps.Gifts.CollectiblePreview(ctx, saved.GiftID) + if err != nil { + return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, internalErr() + } + if !found || preview.UpgradeStars <= 0 || preview.Issued >= preview.SupplyTotal { + return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, starGiftInvalidErr() + } + return saved, preview, nil +} + +func (r *Router) tgStarGiftUpgradeUpdates(ctx context.Context, ownerUserID int64, result domain.StarGiftUpgradeResult, includeBalance bool) *tg.Updates { + message, event := result.Send.RecipientMessage, result.Send.RecipientEvent + if result.Send.SenderMessage.OwnerUserID == ownerUserID { + message, event = result.Send.SenderMessage, result.Send.SenderEvent + } + updates := tgPrivateMessageUpdates(event, message, 0, false, + r.usersForMessageUpdate(ctx, ownerUserID, message), + r.chatsForMessageUpdate(ctx, ownerUserID, message)) + if includeBalance { + updates.Updates = append(updates.Updates, &tg.UpdateStarsBalance{Balance: &tg.StarsAmount{Amount: result.Balance.Balance}}) + } + return updates +} + +func starGiftUpgradeFormID(userID, savedGiftID, stars int64, keepOriginal bool) int64 { + id := userID*0x9e3779b1 ^ savedGiftID<<11 ^ stars<<19 ^ 0x55504752414445 + if keepOriginal { + id ^= 0x4b454550 + } + if id < 0 { + id = ^id + } + if id == 0 { + id = 1 + } + return id +} + +func starGiftUpgradeErr(err error) error { + switch { + case errors.Is(err, domain.ErrStarsInsufficient): + return starsErr(err) + case errors.Is(err, domain.ErrStarGiftNotFound), + errors.Is(err, domain.ErrStarGiftAlreadyConverted), + errors.Is(err, domain.ErrStarGiftAlreadyUpgraded), + errors.Is(err, domain.ErrStarGiftCollectibleUnavailable), + errors.Is(err, domain.ErrStarGiftCollectibleSoldOut), + errors.Is(err, domain.ErrStarGiftCollectibleInvalid): + return starGiftInvalidErr() + default: + return internalErr() + } +} + +func sessionIDOrZero(ctx context.Context) int64 { + sessionID, _ := SessionIDFrom(ctx) + return sessionID +} + +func (r *Router) onPaymentsGetStarGiftUpgradePreview(ctx context.Context, giftID int64) (*tg.PaymentsStarGiftUpgradePreview, error) { + if giftID <= 0 || r.deps.Gifts == nil { + return nil, starGiftInvalidErr() + } + preview, found, err := r.deps.Gifts.CollectiblePreview(ctx, giftID) + if err != nil { + return nil, internalErr() + } + if !found || preview.Issued >= preview.SupplyTotal { + return nil, starGiftInvalidErr() + } + return &tg.PaymentsStarGiftUpgradePreview{ + SampleAttributes: tgStarGiftPreviewAttributes(preview), + Prices: []tg.StarGiftUpgradePrice{}, + NextPrices: []tg.StarGiftUpgradePrice{}, + }, nil +} + +func (r *Router) onPaymentsGetUniqueStarGift(ctx context.Context, slug string) (*tg.PaymentsUniqueStarGift, error) { + if r.deps.Gifts == nil || strings.TrimSpace(slug) == "" { + return nil, starGiftInvalidErr() + } + viewerUserID, _, err := r.currentUserID(ctx) + if err != nil { + return nil, internalErr() + } + unique, found, err := r.deps.Gifts.UniqueBySlug(ctx, slug) + if err != nil { + return nil, internalErr() + } + if !found { + return nil, starGiftInvalidErr() + } + out := &tg.PaymentsUniqueStarGift{ + Gift: tgUniqueStarGift(unique), + Chats: []tg.ChatClass{}, + Users: []tg.UserClass{}, + } + switch unique.Owner.Type { + case domain.PeerTypeUser: + ids := []int64{unique.Owner.ID} + if unique.KeepOriginalDetails && !unique.OriginalNameHidden && unique.OriginalFromUserID != 0 && unique.OriginalFromUserID != unique.Owner.ID { + ids = append(ids, unique.OriginalFromUserID) + } + out.Users = tgUsersForViewer(viewerUserID, r.domainUsersForIDs(ctx, viewerUserID, ids)) + case domain.PeerTypeChannel: + out.Chats = r.tgChatsForChannelIDs(ctx, viewerUserID, []int64{unique.Owner.ID}) + } + return out, nil +} + +func tgStarGiftPreviewAttributes(preview domain.StarGiftUpgradePreview) []tg.StarGiftAttributeClass { + out := make([]tg.StarGiftAttributeClass, 0, len(preview.Models)+len(preview.Patterns)+len(preview.Backdrops)) + for _, attribute := range preview.Models { + out = append(out, tgStarGiftAttribute(attribute)) + } + for _, attribute := range preview.Patterns { + out = append(out, tgStarGiftAttribute(attribute)) + } + for _, attribute := range preview.Backdrops { + out = append(out, tgStarGiftAttribute(attribute)) + } + return out +} + +func tgStarGiftAttribute(attribute domain.StarGiftCollectibleAttribute) tg.StarGiftAttributeClass { + rarity := &tg.StarGiftAttributeRarity{Permille: attribute.RarityPermille} + switch attribute.Kind { + case domain.StarGiftCollectibleModel: + document := tg.DocumentClass(&tg.DocumentEmpty{}) + if attribute.Document != nil { + document = tgDocument(*attribute.Document) + } + return &tg.StarGiftAttributeModel{Name: attribute.Name, Document: document, Rarity: rarity} + case domain.StarGiftCollectiblePattern: + document := tg.DocumentClass(&tg.DocumentEmpty{}) + if attribute.Document != nil { + document = tgDocument(*attribute.Document) + } + return &tg.StarGiftAttributePattern{Name: attribute.Name, Document: document, Rarity: rarity} + case domain.StarGiftCollectibleBackdrop: + return &tg.StarGiftAttributeBackdrop{ + Name: attribute.Name, BackdropID: attribute.BackdropID, + CenterColor: attribute.CenterColor, EdgeColor: attribute.EdgeColor, + PatternColor: attribute.PatternColor, TextColor: attribute.TextColor, Rarity: rarity, + } + default: + return &tg.StarGiftAttributeBackdrop{Name: attribute.Name, Rarity: rarity} + } +} + +func tgUniqueStarGift(unique domain.UniqueStarGift) *tg.StarGiftUnique { + attributes := []tg.StarGiftAttributeClass{ + tgStarGiftAttribute(unique.Model), + tgStarGiftAttribute(unique.Pattern), + tgStarGiftAttribute(unique.Backdrop), + } + if unique.KeepOriginalDetails && unique.OriginalOwner.ID != 0 { + original := &tg.StarGiftAttributeOriginalDetails{ + RecipientID: tgPeer(unique.OriginalOwner), + Date: unique.OriginalDate, + } + if unique.OriginalFromUserID != 0 && !unique.OriginalNameHidden { + original.SetSenderID(&tg.PeerUser{UserID: unique.OriginalFromUserID}) + } + if unique.OriginalMessage != "" { + original.SetMessage(tg.TextWithEntities{Text: unique.OriginalMessage}) + } + attributes = append(attributes, original) + } + out := &tg.StarGiftUnique{ + ID: unique.ID, GiftID: unique.GiftID, Title: unique.Title, Slug: unique.Slug, Num: unique.Num, + Attributes: attributes, AvailabilityIssued: unique.AvailabilityIssued, AvailabilityTotal: unique.AvailabilityTotal, + } + if owner := tgPeer(unique.Owner); owner != nil { + out.SetOwnerID(owner) + } + return out +} diff --git a/internal/rpc/payments_star_gifts.go b/internal/rpc/payments_star_gifts.go index de8ec3f1..28a3a278 100644 --- a/internal/rpc/payments_star_gifts.go +++ b/internal/rpc/payments_star_gifts.go @@ -77,6 +77,9 @@ func (r *Router) onPaymentsGetPaymentForm(ctx context.Context, req *tg.PaymentsG } return r.starsTopupPaymentForm(userID, purpose), nil } + if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftUpgrade); ok { + return r.starGiftUpgradePaymentForm(ctx, userID, inv) + } inv, ok := req.Invoice.(*tg.InputInvoiceStarGift) if !ok { @@ -85,18 +88,32 @@ func (r *Router) onPaymentsGetPaymentForm(ctx context.Context, req *tg.PaymentsG if r.deps.Gifts == nil { return nil, notImplementedErr() } - if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inv.Peer); err != nil { + peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inv.Peer) + if err != nil { return nil, err } + if inv.IncludeUpgrade && peer.Type != domain.PeerTypeUser { + // Channel upgrades remain blocked until they can advance channel pts and + // publish a durable channel update. Never collect a prepaid upgrade that + // the recipient cannot consume. + return nil, starGiftInvalidErr() + } gift, err := r.starGiftFromCatalog(ctx, inv.GiftID) if err != nil { return nil, err } + upgradeStars := int64(0) + if inv.IncludeUpgrade { + if gift.UpgradeStars <= 0 || gift.UpgradeIssued >= gift.UpgradeTotal { + return nil, starGiftInvalidErr() + } + upgradeStars = gift.UpgradeStars + } return &tg.PaymentsPaymentFormStarGift{ - FormID: starGiftFormID(userID, inv.GiftID), + FormID: starGiftFormIDWithUpgrade(userID, peer, gift, inv.IncludeUpgrade), Invoice: tg.Invoice{ Currency: "XTR", - Prices: []tg.LabeledPrice{{Label: giftPriceLabel(gift), Amount: gift.Stars}}, + Prices: []tg.LabeledPrice{{Label: giftPriceLabel(gift), Amount: gift.Stars + upgradeStars}}, }, }, nil } @@ -119,6 +136,9 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe if inv, ok := req.Invoice.(*tg.InputInvoiceStars); ok { return r.sendStarsTopupForm(ctx, userID, req.FormID, inv) } + if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftUpgrade); ok { + return r.sendStarGiftUpgradeForm(ctx, userID, req.FormID, inv) + } inv, ok := req.Invoice.(*tg.InputInvoiceStarGift) if !ok { @@ -131,6 +151,9 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe if (peer.Type != domain.PeerTypeUser && peer.Type != domain.PeerTypeChannel) || peer.ID == 0 { return nil, peerIDInvalidErr() } + if inv.IncludeUpgrade && peer.Type != domain.PeerTypeUser { + return nil, starGiftInvalidErr() + } if r.deps.Stars == nil || r.deps.Gifts == nil { return nil, notImplementedErr() } @@ -144,13 +167,24 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe if err != nil { return nil, err } + upgradeStars := int64(0) + if inv.IncludeUpgrade { + if gift.UpgradeStars <= 0 || gift.UpgradeIssued >= gift.UpgradeTotal { + return nil, starGiftInvalidErr() + } + upgradeStars = gift.UpgradeStars + } + if req.FormID != starGiftFormIDWithUpgrade(userID, peer, gift, inv.IncludeUpgrade) { + return nil, starsFormAmountMismatchErr() + } giftMessage := "" if m, ok := inv.GetMessage(); ok { giftMessage = clampGiftMessage(m.Text) } // 1. Debit 送礼人(不足→BALANCE_TOO_LOW)。 - balance, err := r.deps.Stars.Debit(ctx, userID, gift.Stars, domain.StarsReasonGift, peer, "Star gift", gift.Title) + purchaseStars := gift.Stars + upgradeStars + balance, err := r.deps.Stars.Debit(ctx, userID, purchaseStars, domain.StarsReasonGift, peer, "Star gift", gift.Title) if err != nil { return nil, starsErr(err) } @@ -158,14 +192,14 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe var updates *tg.Updates switch peer.Type { case domain.PeerTypeUser: - updates, err = r.sendStarGiftToUser(ctx, userID, peer.ID, gift, inv.HideName, giftMessage) + updates, err = r.sendStarGiftToUser(ctx, userID, peer.ID, gift, inv.HideName, giftMessage, upgradeStars) case domain.PeerTypeChannel: updates, err = r.sendStarGiftToChannel(ctx, userID, peer.ID, gift, inv.HideName, giftMessage) default: err = domain.ErrStarGiftInvalid } if err != nil { - r.refundStarGift(ctx, userID, peer, gift) + r.refundStarGift(ctx, userID, peer, gift, purchaseStars) return nil, internalErr() } @@ -260,23 +294,25 @@ func (r *Router) sendStarsTopupForm(ctx context.Context, userID, formID int64, i return &tg.PaymentsPaymentResult{Updates: starsBalanceUpdates(balance.Balance, r.clock.Now().Unix())}, nil } -func (r *Router) sendStarGiftToUser(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string) (*tg.Updates, error) { +func (r *Router) sendStarGiftToUser(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string, prepaidUpgradeStars int64) (*tg.Updates, error) { // 2. 投递礼物服务消息到收礼人私聊(双盒 + 推送)。 - send, err := r.deliverStarGift(ctx, senderID, recipientID, gift, hideName, message) + send, err := r.deliverStarGift(ctx, senderID, recipientID, gift, hideName, message, prepaidUpgradeStars) if err != nil { return nil, err } // 3. 记账:收礼人收到一份礼物实例(msg_id = 收礼人侧消息 id)。 if _, err := r.deps.Gifts.RecordSavedGift(ctx, domain.SavedStarGift{ - Owner: domain.Peer{Type: domain.PeerTypeUser, ID: recipientID}, - FromUserID: senderID, - GiftID: gift.ID, - MsgID: send.RecipientMessage.ID, - Date: send.RecipientMessage.Date, - NameHidden: hideName, - Unsaved: false, - ConvertStars: gift.ConvertStars, - Message: message, + Owner: domain.Peer{Type: domain.PeerTypeUser, ID: recipientID}, + FromUserID: senderID, + GiftID: gift.ID, + RevisionID: gift.RevisionID, + MsgID: send.RecipientMessage.ID, + Date: send.RecipientMessage.Date, + NameHidden: hideName, + Unsaved: false, + ConvertStars: gift.ConvertStars, + PrepaidUpgradeStars: prepaidUpgradeStars, + Message: message, }); err != nil { return nil, err } @@ -294,21 +330,25 @@ func (r *Router) sendStarGiftToChannel(ctx context.Context, senderID, channelID action := domain.ChannelMessageAction{ Type: domain.ChannelActionStarGift, StarGift: &domain.MessageStarGiftAction{ - GiftID: gift.ID, - Stars: gift.Stars, - ConvertStars: gift.ConvertStars, - Title: gift.Title, - Sticker: &sticker, - Message: message, - FromUserID: senderID, - NameHidden: hideName, - Saved: true, + GiftID: gift.ID, + Stars: gift.Stars, + ConvertStars: gift.ConvertStars, + Title: gift.Title, + Sticker: &sticker, + Message: message, + FromUserID: senderID, + NameHidden: hideName, + Saved: true, + CanUpgrade: false, + PrepaidUpgrade: false, + UpgradeStars: 0, }, } savedID, err := r.deps.Gifts.RecordSavedGift(ctx, domain.SavedStarGift{ Owner: domain.Peer{Type: domain.PeerTypeChannel, ID: channelID}, FromUserID: senderID, GiftID: gift.ID, + RevisionID: gift.RevisionID, MsgID: 0, SavedID: 0, Date: now, @@ -335,7 +375,7 @@ func (r *Router) sendStarGiftToChannel(ctx context.Context, senderID, channelID } // deliverStarGift 经 SendPrivateText 把 messageActionStarGift 服务消息投递到收礼人私聊。 -func (r *Router) deliverStarGift(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string) (domain.SendPrivateTextResult, error) { +func (r *Router) deliverStarGift(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string, prepaidUpgradeStars int64) (domain.SendPrivateTextResult, error) { recipientBlocked, err := r.peerBlocksUser(ctx, senderID, recipientID) if err != nil { return domain.SendPrivateTextResult{}, err @@ -347,16 +387,19 @@ func (r *Router) deliverStarGift(ctx context.Context, senderID, recipientID int6 ServiceAction: &domain.MessageServiceAction{ Kind: domain.MessageServiceActionStarGift, StarGift: &domain.MessageStarGiftAction{ - GiftID: gift.ID, - Stars: gift.Stars, - ConvertStars: gift.ConvertStars, - Title: gift.Title, - Sticker: &sticker, - Message: message, - FromUserID: senderID, - PeerUserID: recipientID, - NameHidden: hideName, - Saved: true, + GiftID: gift.ID, + Stars: gift.Stars, + ConvertStars: gift.ConvertStars, + Title: gift.Title, + Sticker: &sticker, + Message: message, + FromUserID: senderID, + PeerUserID: recipientID, + NameHidden: hideName, + Saved: true, + CanUpgrade: gift.UpgradeStars > 0, + PrepaidUpgrade: prepaidUpgradeStars > 0, + UpgradeStars: gift.UpgradeStars, }, }, } @@ -374,8 +417,8 @@ func (r *Router) deliverStarGift(ctx context.Context, senderID, recipientID int6 } // refundStarGift 补偿退款(投递/记账失败时把已 Debit 的星退回)。 -func (r *Router) refundStarGift(ctx context.Context, userID int64, peer domain.Peer, gift domain.StarGift) { - if _, err := r.deps.Stars.Credit(ctx, userID, gift.Stars, domain.StarsReasonGift, peer, "Star gift refund", gift.Title); err != nil { +func (r *Router) refundStarGift(ctx context.Context, userID int64, peer domain.Peer, gift domain.StarGift, amount int64) { + if _, err := r.deps.Stars.Credit(ctx, userID, amount, domain.StarsReasonGift, peer, "Star gift refund", gift.Title); err != nil { r.log.Error("star gift refund failed", zap.Int64("user_id", userID), zap.Int64("gift_id", gift.ID), zap.Error(err)) } } @@ -396,11 +439,27 @@ func (r *Router) onPaymentsGetSavedStarGifts(ctx context.Context, req *tg.Paymen if r.deps.Gifts == nil { return emptySavedStarGifts(), nil } - page, err := r.deps.Gifts.ListSaved(ctx, owner, req.ExcludeUnsaved, req.Offset, req.Limit) + collectionID, _ := req.GetCollectionID() + page, err := r.deps.Gifts.ListSavedFiltered(ctx, domain.SavedStarGiftFilter{ + Owner: owner, + ExcludeUnsaved: req.ExcludeUnsaved, + ExcludeSaved: req.ExcludeSaved, + ExcludeUnlimited: req.ExcludeUnlimited, + ExcludeUnique: req.ExcludeUnique, + ExcludeUpgradable: req.ExcludeUpgradable, + ExcludeUnupgradable: req.ExcludeUnupgradable, + CollectionID: collectionID, + Offset: req.Offset, + Limit: req.Limit, + }) if err != nil { return nil, internalErr() } - return r.tgSavedStarGiftsResponse(ctx, userID, page.Gifts, page.Count, page.NextOffset), nil + response, err := r.tgSavedStarGiftsResponse(ctx, userID, page.Gifts, page.Count, page.NextOffset) + if err != nil { + return nil, internalErr() + } + return response, nil } // onPaymentsGetSavedStarGift 按 InputSavedStarGift 引用取指定礼物。 @@ -429,7 +488,11 @@ func (r *Router) onPaymentsGetSavedStarGift(ctx context.Context, refs []tg.Input gifts = append(gifts, g) } } - return r.tgSavedStarGiftsResponse(ctx, userID, gifts, len(gifts), ""), nil + response, err := r.tgSavedStarGiftsResponse(ctx, userID, gifts, len(gifts), "") + if err != nil { + return nil, internalErr() + } + return response, nil } // onPaymentsSaveStarGift 切换礼物在资料的展示(unsave=true 隐藏)。 @@ -565,9 +628,15 @@ func (r *Router) starGiftRefFromInput(ctx context.Context, userID int64, ref tg. } func (r *Router) ensureCanManageStarGiftOwner(ctx context.Context, userID int64, owner domain.Peer) error { - if owner.Type != domain.PeerTypeChannel { + if owner.Type == domain.PeerTypeUser { + if owner.ID != userID { + return peerIDInvalidErr() + } return nil } + if owner.Type != domain.PeerTypeChannel { + return peerIDInvalidErr() + } if r.deps.Channels == nil { return notImplementedErr() } @@ -590,11 +659,41 @@ func (r *Router) invalidateStarGiftOwnerProjection(owner domain.Peer) { } } -func (r *Router) tgSavedStarGiftsResponse(ctx context.Context, viewerUserID int64, gifts []domain.SavedStarGift, count int, nextOffset string) *tg.PaymentsSavedStarGifts { - catalog := r.resolveStarGiftCatalog(ctx, gifts) +func (r *Router) tgSavedStarGiftsResponse(ctx context.Context, viewerUserID int64, gifts []domain.SavedStarGift, count int, nextOffset string) (*tg.PaymentsSavedStarGifts, error) { + uniqueIDs := make([]int64, 0) + seenUnique := make(map[int64]struct{}) + for _, gift := range gifts { + if gift.UniqueGiftID != 0 { + if _, seen := seenUnique[gift.UniqueGiftID]; !seen { + seenUnique[gift.UniqueGiftID] = struct{}{} + uniqueIDs = append(uniqueIDs, gift.UniqueGiftID) + } + } + } + if len(uniqueIDs) > 0 { + uniques, err := r.deps.Gifts.UniqueByIDs(ctx, uniqueIDs) + if err != nil { + return nil, err + } + for i := range gifts { + if unique, ok := uniques[gifts[i].UniqueGiftID]; ok { + copy := unique + gifts[i].Unique = © + } + } + } + catalog, err := r.resolveStarGiftCatalog(ctx, gifts) + if err != nil { + return nil, err + } + availability, err := r.resolveStarGiftCollectibleAvailability(ctx, gifts) + if err != nil { + return nil, err + } + projected := tgSavedStarGifts(gifts, catalog, availability) out := &tg.PaymentsSavedStarGifts{ Count: count, - Gifts: tgSavedStarGifts(gifts, catalog), + Gifts: projected, Chats: []tg.ChatClass{}, } if ids := savedStarGiftUserIDs(gifts); len(ids) > 0 { @@ -605,7 +704,35 @@ func (r *Router) tgSavedStarGiftsResponse(ctx context.Context, viewerUserID int6 if nextOffset != "" { out.SetNextOffset(nextOffset) } - return out + return out, nil +} + +func (r *Router) resolveStarGiftCollectibleAvailability(ctx context.Context, gifts []domain.SavedStarGift) (map[int64]domain.StarGiftCollectibleAvailability, error) { + out := make(map[int64]domain.StarGiftCollectibleAvailability) + if r.deps.Gifts == nil { + return out, nil + } + ids := make([]int64, 0, len(gifts)) + seen := make(map[int64]struct{}, len(gifts)) + for _, gift := range gifts { + if gift.UniqueGiftID != 0 { + continue + } + if gift.Owner.Type != domain.PeerTypeUser { + // Channel upgrade RPCs are deliberately blocked until the channel pts + // aggregate exists, so do not advertise a dead-end action. + continue + } + if _, ok := seen[gift.GiftID]; ok { + continue + } + seen[gift.GiftID] = struct{}{} + ids = append(ids, gift.GiftID) + } + if len(ids) == 0 { + return out, nil + } + return r.deps.Gifts.CollectibleAvailability(ctx, ids) } func emptySavedStarGifts() *tg.PaymentsSavedStarGifts { @@ -636,6 +763,9 @@ func tgStarGift(g domain.StarGift) *tg.StarGift { if g.Title != "" { gift.SetTitle(g.Title) } + if g.UpgradeStars > 0 && g.UpgradeIssued < g.UpgradeTotal { + gift.SetUpgradeStars(g.UpgradeStars) + } return gift } @@ -667,6 +797,14 @@ func tgMessageActionStarGift(in *domain.MessageStarGiftAction) tg.MessageActionC if in.Converted { action.Converted = true } + action.CanUpgrade = in.CanUpgrade + action.PrepaidUpgrade = in.PrepaidUpgrade + if in.UpgradeStars > 0 { + action.SetUpgradeStars(in.UpgradeStars) + } + if in.UpgradeMsgID > 0 { + action.SetUpgradeMsgID(in.UpgradeMsgID) + } if in.ConvertStars > 0 { action.SetConvertStars(in.ConvertStars) } @@ -687,30 +825,38 @@ func tgMessageActionStarGift(in *domain.MessageStarGiftAction) tg.MessageActionC return action } -// resolveStarGiftCatalog 解析这批 saved gift 涉及的目录项(giftID → StarGift),供下发完整贴纸/星价。 -func (r *Router) resolveStarGiftCatalog(ctx context.Context, gifts []domain.SavedStarGift) map[int64]domain.StarGift { +// resolveStarGiftCatalog 解析这批 saved gift 涉及的不可变目录版本(revisionID → StarGift)。 +func (r *Router) resolveStarGiftCatalog(ctx context.Context, gifts []domain.SavedStarGift) (map[int64]domain.StarGift, error) { out := make(map[int64]domain.StarGift, len(gifts)) if r.deps.Gifts == nil { - return out + return out, nil } for _, g := range gifts { - if _, ok := out[g.GiftID]; ok { + if g.RevisionID == 0 { + return nil, domain.ErrStarGiftInvalid + } + if _, ok := out[g.RevisionID]; ok { continue } - if gift, found, err := r.deps.Gifts.GiftByID(ctx, g.GiftID); err == nil && found { - out[g.GiftID] = gift + gift, found, err := r.deps.Gifts.GiftRevisionByID(ctx, g.RevisionID) + if err != nil { + return nil, err } + if !found { + return nil, domain.ErrStarGiftInvalid + } + out[g.RevisionID] = gift } - return out + return out, nil } // tgSavedStarGifts 把已收到礼物实例投影为 []tg.SavedStarGift。 -func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.StarGift) []tg.SavedStarGift { +func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.StarGift, availability map[int64]domain.StarGiftCollectibleAvailability) []tg.SavedStarGift { out := make([]tg.SavedStarGift, 0, len(gifts)) for _, g := range gifts { item := tg.SavedStarGift{ Date: g.Date, - Gift: tgSavedStarGiftGift(g, catalog), + Gift: tgSavedStarGiftGift(g, catalog, availability), } if g.NameHidden { item.NameHidden = true @@ -733,19 +879,47 @@ func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.Sta if g.Message != "" { item.SetMessage(tg.TextWithEntities{Text: g.Message}) } + if g.UniqueGiftID == 0 { + current, available := availability[g.GiftID] + canIssue := available && current.UpgradeStars > 0 && current.Issued < current.SupplyTotal + if canIssue { + item.CanUpgrade = true + } + if g.PrepaidUpgradeStars > 0 && canIssue { + item.SetUpgradeStars(g.PrepaidUpgradeStars) + item.CanUpgrade = true + } + } + if g.PinnedOrder > 0 { + item.PinnedToTop = true + } + if len(g.CollectionIDs) > 0 { + item.SetCollectionID(append([]int(nil), g.CollectionIDs...)) + } + if g.Unique != nil { + item.SetGiftNum(g.Unique.Num) + } out = append(out, item) } return out } -// tgSavedStarGiftGift 把 SavedStarGift 内嵌礼物投影为 tg.StarGift:优先用目录解析出完整贴纸/星价 -// (客户端据有效 sticker 渲染),目录缺失(礼物已下架)时兜底最小形态。convert_stars 用实例值。 -func tgSavedStarGiftGift(g domain.SavedStarGift, catalog map[int64]domain.StarGift) tg.StarGiftClass { - if gift, ok := catalog[g.GiftID]; ok { +// tgSavedStarGiftGift 按收到时的不可变 revision 投影,目录停用或后续改版不影响历史显示。 +func tgSavedStarGiftGift(g domain.SavedStarGift, catalog map[int64]domain.StarGift, availability map[int64]domain.StarGiftCollectibleAvailability) tg.StarGiftClass { + if g.Unique != nil { + return tgUniqueStarGift(*g.Unique) + } + if gift, ok := catalog[g.RevisionID]; ok { + if current, ok := availability[g.GiftID]; ok { + gift.UpgradeStars = current.UpgradeStars + gift.UpgradeTotal = current.SupplyTotal + gift.UpgradeIssued = current.Issued + } out := tgStarGift(gift) out.ConvertStars = g.ConvertStars return out } + // resolveStarGiftCatalog 在进入投影前保证每个 revision 都存在;该分支仅保留类型完备性。 return &tg.StarGift{ ID: g.GiftID, Sticker: &tg.DocumentEmpty{}, @@ -770,8 +944,18 @@ func savedStarGiftUserIDs(gifts []domain.SavedStarGift) []int64 { return ids } -func starGiftFormID(userID, giftID int64) int64 { - id := userID*0x9e3779b1 ^ (giftID << 7) ^ 0x5347494654 +func starGiftFormID(userID int64, peer domain.Peer, gift domain.StarGift) int64 { + return starGiftFormIDWithUpgrade(userID, peer, gift, false) +} + +func starGiftFormIDWithUpgrade(userID int64, peer domain.Peer, gift domain.StarGift, includeUpgrade bool) int64 { + id := userID*0x9e3779b1 ^ (gift.ID << 7) ^ (gift.RevisionID << 11) ^ (gift.Stars << 17) ^ (peer.ID << 23) ^ 0x5347494654 + if includeUpgrade { + id ^= gift.UpgradeStars<<29 ^ 0x55504752414445 + } + for _, ch := range string(peer.Type) { + id = id*131 + int64(ch) + } if id == 0 { id = 0x5347 } diff --git a/internal/rpc/payments_star_gifts_rpc_test.go b/internal/rpc/payments_star_gifts_rpc_test.go index 4b1ce168..0c714bdd 100644 --- a/internal/rpc/payments_star_gifts_rpc_test.go +++ b/internal/rpc/payments_star_gifts_rpc_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/iamxvbaba/td/bin" "github.com/iamxvbaba/td/clock" "github.com/iamxvbaba/td/tg" "github.com/iamxvbaba/td/tgerr" @@ -18,12 +19,6 @@ import ( "telesrv/internal/store/memory" ) -type stubGiftCatalog struct{ gifts []domain.StarGift } - -func (s stubGiftCatalog) BuildStarGiftCatalog(_ context.Context) ([]domain.StarGift, error) { - return s.gifts, nil -} - func starGiftTestRouter(t *testing.T) (*Router, domain.User, domain.User, domain.StarGift) { t.Helper() ctx := context.Background() @@ -40,10 +35,12 @@ func starGiftTestRouter(t *testing.T) (*Router, domain.User, domain.User, domain t.Fatalf("create recipient: %v", err) } gift := domain.StarGift{ - ID: 8001, Stars: 50, ConvertStars: 50, Title: "Cake", - Sticker: domain.Document{ID: 700, AccessHash: 7, DCID: 2, MimeType: "image/webp"}, + ID: 8001, RevisionID: 9001, Stars: 50, ConvertStars: 50, Title: "Cake", + Sticker: domain.Document{ID: 700, AccessHash: 7, DCID: 2, MimeType: "application/x-tgsticker", Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}}, } - gifts := appstargifts.NewService(memory.NewStarGiftStore(), stubGiftCatalog{[]domain.StarGift{gift}}) + giftStore := memory.NewStarGiftStore() + giftStore.SeedCatalog([]domain.StarGift{gift}) + gifts := appstargifts.NewService(giftStore, nil, 2) r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{ Users: appusers.NewService(users), Messages: appmessages.NewService(msgStore, dialogs), @@ -54,6 +51,328 @@ func starGiftTestRouter(t *testing.T) (*Router, domain.User, domain.User, domain return r, sender, recipient, gift } +type uniqueGiftRPCService struct { + GiftsService + unique domain.UniqueStarGift +} + +func (s *uniqueGiftRPCService) UniqueBySlug(_ context.Context, slug string) (domain.UniqueStarGift, bool, error) { + return s.unique, slug == s.unique.Slug, nil +} + +func collectibleRPCAttribute(kind domain.StarGiftCollectibleAttributeKind, id int64, name string) domain.StarGiftCollectibleAttribute { + attribute := domain.StarGiftCollectibleAttribute{Kind: kind, Name: name, RarityPermille: 1000} + if kind == domain.StarGiftCollectibleBackdrop { + attribute.BackdropID = int(id) + attribute.CenterColor = 0x112233 + attribute.EdgeColor = 0x223344 + attribute.PatternColor = 0x334455 + attribute.TextColor = 0xffffff + return attribute + } + attribute.Document = &domain.Document{ + ID: id, AccessHash: id + 1, FileReference: []byte("collectible-rpc"), Date: 1700000000, + MimeType: "application/x-tgsticker", Size: 3, DCID: 2, + Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}, {Kind: domain.DocAttrFilename, FileName: "gift.tgs"}}, + } + attribute.Animation = &domain.StarGiftAnimation{ + SourceName: "gift.tgs", SourceFormat: domain.StarGiftAnimationTGS, + JSON: []byte(`{"v":"5.7"}`), TGS: []byte("tgs"), SHA256: make([]byte, 32), Width: 512, Height: 512, + } + attribute.Blob = &domain.FileBlob{LocationKey: "doc:test", Backend: domain.MediaBackendLocalFS, ObjectKey: "test", Size: 3} + return attribute +} + +func TestSavedStarGiftProjectionCombinesHistoricalCatalogWithCurrentCollectibleAvailability(t *testing.T) { + historical := domain.StarGift{ + ID: 8001, RevisionID: 9001, Stars: 50, ConvertStars: 25, Title: "Historical Cake", + Sticker: domain.Document{ID: 700, AccessHash: 7, DCID: 2, MimeType: "application/x-tgsticker"}, + } + saved := domain.SavedStarGift{GiftID: historical.ID, RevisionID: historical.RevisionID, MsgID: 44, Date: 100, ConvertStars: historical.ConvertStars} + availability := map[int64]domain.StarGiftCollectibleAvailability{ + historical.ID: {UpgradeStars: 75, SupplyTotal: 500, Issued: 12}, + } + + projected := tgSavedStarGifts([]domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability) + if len(projected) != 1 || !projected[0].CanUpgrade { + t.Fatalf("saved gift = %#v, want current pool to make historical gift upgradable", projected) + } + gift, ok := projected[0].Gift.(*tg.StarGift) + if !ok { + t.Fatalf("saved gift inner = %T, want *tg.StarGift", projected[0].Gift) + } + if gift.Title != historical.Title || gift.Stars != historical.Stars || gift.ConvertStars != historical.ConvertStars { + t.Fatalf("historical snapshot changed: %#v", gift) + } + if upgradeStars, ok := gift.GetUpgradeStars(); !ok || upgradeStars != 75 { + t.Fatalf("upgrade_stars = %d ok=%v, want current price 75", upgradeStars, ok) + } + for _, profile := range []tg.LayerProfile{tg.LayerProfile227, tg.LayerProfile228} { + wire := &tg.PaymentsSavedStarGifts{Count: 1, Gifts: projected, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}} + encoded := &bin.Buffer{} + if err := tg.EncodeLayer(profile, tg.LayerConstructorPaymentsSavedStarGiftsType(), wire, encoded); err != nil { + t.Fatalf("encode Layer %d saved gift: %v", profile, err) + } + decoded, err := tg.DecodeLayer(profile, tg.LayerConstructorPaymentsSavedStarGiftsType(), &bin.Buffer{Buf: encoded.Buf}) + if err != nil { + t.Fatalf("decode Layer %d saved gift: %v", profile, err) + } + inner, ok := decoded.Gifts[0].Gift.(*tg.StarGift) + if !ok || !decoded.Gifts[0].CanUpgrade || inner.UpgradeStars != 75 { + t.Fatalf("Layer %d projection lost upgrade flags: %#v", profile, decoded.Gifts[0]) + } + } + + availability[historical.ID] = domain.StarGiftCollectibleAvailability{UpgradeStars: 75, SupplyTotal: 500, Issued: 500} + soldOut := tgSavedStarGifts([]domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability)[0] + if soldOut.CanUpgrade { + t.Fatal("sold-out collectible pool must not advertise upgrade") + } + if gift, ok := soldOut.Gift.(*tg.StarGift); !ok { + t.Fatalf("sold-out inner = %T, want *tg.StarGift", soldOut.Gift) + } else if _, ok := gift.GetUpgradeStars(); ok { + t.Fatal("sold-out catalog projection must not expose upgrade_stars") + } + saved.PrepaidUpgradeStars = 75 + soldOutPrepaid := tgSavedStarGifts([]domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability)[0] + if soldOutPrepaid.CanUpgrade { + t.Fatal("sold-out prepaid gift must not advertise an upgrade the aggregate will reject") + } + if _, ok := soldOutPrepaid.GetUpgradeStars(); ok { + t.Fatal("sold-out prepaid gift must not expose stale prepaid upgrade_stars") + } +} + +func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *testing.T) { + r, sender, owner, gift := starGiftTestRouter(t) + ctx := context.Background() + ownerCtx := WithUserID(ctx, owner.ID) + giftService, ok := r.deps.Gifts.(*appstargifts.Service) + if !ok { + t.Fatalf("gift service = %T", r.deps.Gifts) + } + model := collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8101, "Aurora") + pattern := collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8102, "Orbit") + backdrop := collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 1, "Midnight") + if _, err := giftService.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{ + GiftID: gift.ID, UpgradeStars: 75, SupplyTotal: 500, SlugPrefix: "cake", + Models: []domain.StarGiftCollectibleAttribute{model}, Patterns: []domain.StarGiftCollectibleAttribute{pattern}, + Backdrops: []domain.StarGiftCollectibleAttribute{backdrop}, Actor: "test", CommandID: "collectible-rpc", + }); err != nil { + t.Fatalf("publish collectible pool: %v", err) + } + if _, err := r.deps.Gifts.RecordSavedGift(ctx, domain.SavedStarGift{ + Owner: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}, FromUserID: sender.ID, + GiftID: gift.ID, RevisionID: gift.RevisionID, MsgID: 444, Date: 1700000000, ConvertStars: gift.ConvertStars, + }); err != nil { + t.Fatalf("record upgrade target: %v", err) + } + + preview, err := r.onPaymentsGetStarGiftUpgradePreview(ownerCtx, gift.ID) + if err != nil || len(preview.SampleAttributes) != 3 { + t.Fatalf("upgrade preview = %#v err %v", preview, err) + } + invoice := &tg.InputInvoiceStarGiftUpgrade{Stargift: &tg.InputSavedStarGiftUser{MsgID: 444}} + formClass, err := r.onPaymentsGetPaymentForm(ownerCtx, &tg.PaymentsGetPaymentFormRequest{Invoice: invoice}) + if err != nil { + t.Fatalf("get upgrade payment form: %v", err) + } + form, ok := formClass.(*tg.PaymentsPaymentFormStarGift) + if !ok || form.FormID == 0 || form.Invoice.Currency != "XTR" || len(form.Invoice.Prices) != 1 || form.Invoice.Prices[0].Amount != 75 { + t.Fatalf("upgrade payment form = %T %#v", formClass, formClass) + } + + unique := domain.UniqueStarGift{ + ID: 9200000000000001, GiftID: gift.ID, Title: gift.Title, Slug: "cake-1", Num: 1, + Owner: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}, + Model: model, Pattern: pattern, Backdrop: backdrop, + AvailabilityIssued: 1, AvailabilityTotal: 500, KeepOriginalDetails: true, + OriginalFromUserID: sender.ID, OriginalOwner: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}, + OriginalDate: 1700000000, OriginalMessage: "hello", + } + r.deps.Gifts = &uniqueGiftRPCService{GiftsService: r.deps.Gifts, unique: unique} + uniqueResponse, err := r.onPaymentsGetUniqueStarGift(WithUserID(ctx, sender.ID), unique.Slug) + if err != nil { + t.Fatalf("get unique gift = %#v err %v", uniqueResponse, err) + } + uniqueGift, ok := uniqueResponse.Gift.(*tg.StarGiftUnique) + if !ok || uniqueGift.Slug != unique.Slug || len(uniqueGift.Attributes) != 4 || len(uniqueResponse.Users) != 2 { + t.Fatalf("get unique gift = %#v", uniqueResponse) + } + + message := domain.Message{Media: &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: &domain.MessageServiceAction{ + Kind: domain.MessageServiceActionStarGiftUnique, + StarGiftUnique: &domain.MessageStarGiftUniqueAction{ + Gift: unique, FromUserID: sender.ID, Peer: unique.Owner, Upgrade: true, Saved: true, + }, + }}} + action, ok := tgMessageServiceAction(message).(*tg.MessageActionStarGiftUnique) + if !ok { + t.Fatalf("unique service action type = %T", tgMessageServiceAction(message)) + } + projectedGift, giftOK := action.Gift.(*tg.StarGiftUnique) + if !giftOK || !action.Upgrade || !action.Saved || projectedGift.ID != unique.ID || projectedGift.Slug != unique.Slug { + t.Fatalf("unique service action = %#v", tgMessageServiceAction(message)) + } + if peer, ok := action.GetPeer(); !ok { + t.Fatal("unique service action missing owner peer") + } else if user, ok := peer.(*tg.PeerUser); !ok || user.UserID != owner.ID { + t.Fatalf("unique service action peer = %#v", peer) + } + for _, profile := range []tg.LayerProfile{tg.LayerProfile227, tg.LayerProfile228} { + responseWire := &bin.Buffer{} + if err := tg.EncodeLayer(profile, tg.LayerConstructorPaymentsUniqueStarGiftType(), uniqueResponse, responseWire); err != nil { + t.Fatalf("encode Layer %d unique response: %v", profile, err) + } + decodedResponse, err := tg.DecodeLayer(profile, tg.LayerConstructorPaymentsUniqueStarGiftType(), &bin.Buffer{Buf: responseWire.Buf}) + if err != nil { + t.Fatalf("decode Layer %d unique response: %v", profile, err) + } + decodedGift, ok := decodedResponse.Gift.(*tg.StarGiftUnique) + if !ok || decodedGift.Slug != unique.Slug || len(decodedGift.Attributes) != 4 { + t.Fatalf("Layer %d unique response lost fields: %#v", profile, decodedResponse.Gift) + } + + actionWire := &bin.Buffer{} + if err := tg.EncodeLayer(profile, tg.LayerConstructorMessageActionStarGiftUniqueType(), action, actionWire); err != nil { + t.Fatalf("encode Layer %d unique action: %v", profile, err) + } + decodedAction, err := tg.DecodeLayer(profile, tg.LayerConstructorMessageActionStarGiftUniqueType(), &bin.Buffer{Buf: actionWire.Buf}) + if err != nil { + t.Fatalf("decode Layer %d unique action: %v", profile, err) + } + if decodedActionGift, ok := decodedAction.Gift.(*tg.StarGiftUnique); !ok || !decodedAction.Upgrade || decodedActionGift.Slug != unique.Slug { + t.Fatalf("Layer %d unique action lost fields: %#v", profile, decodedAction) + } + } +} + +func TestStarGiftCollectionsCRUDFilterOrderAndPin(t *testing.T) { + r, _, owner, gift := starGiftTestRouter(t) + ctx := WithUserID(context.Background(), owner.ID) + ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID} + for _, msgID := range []int{101, 102} { + if _, err := r.deps.Gifts.RecordSavedGift(context.Background(), domain.SavedStarGift{ + Owner: ownerPeer, GiftID: gift.ID, RevisionID: gift.RevisionID, + MsgID: msgID, Date: 1700000000 + msgID, ConvertStars: gift.ConvertStars, + }); err != nil { + t.Fatalf("record saved gift %d: %v", msgID, err) + } + } + ref101 := tg.InputSavedStarGiftClass(&tg.InputSavedStarGiftUser{MsgID: 101}) + ref102 := tg.InputSavedStarGiftClass(&tg.InputSavedStarGiftUser{MsgID: 102}) + + first, err := r.onPaymentsCreateStarGiftCollection(ctx, &tg.PaymentsCreateStarGiftCollectionRequest{ + Peer: &tg.InputPeerSelf{}, Title: " Favorites ", Stargift: []tg.InputSavedStarGiftClass{ref101}, + }) + if err != nil { + t.Fatalf("create first collection: %v", err) + } + second, err := r.onPaymentsCreateStarGiftCollection(ctx, &tg.PaymentsCreateStarGiftCollectionRequest{ + Peer: &tg.InputPeerSelf{}, Title: "Archive", Stargift: []tg.InputSavedStarGiftClass{ref102}, + }) + if err != nil { + t.Fatalf("create second collection: %v", err) + } + if first.Title != "Favorites" || first.GiftsCount != 1 || second.GiftsCount != 1 { + t.Fatalf("created collections = %#v / %#v", first, second) + } + + listedClass, err := r.onPaymentsGetStarGiftCollections(ctx, &tg.PaymentsGetStarGiftCollectionsRequest{Peer: &tg.InputPeerSelf{}}) + if err != nil { + t.Fatalf("list collections: %v", err) + } + listed, ok := listedClass.(*tg.PaymentsStarGiftCollections) + if !ok || len(listed.Collections) != 2 { + t.Fatalf("list collections = %T %#v, want two", listedClass, listedClass) + } + domainCollections, err := r.deps.Gifts.ListCollections(context.Background(), ownerPeer) + if err != nil { + t.Fatalf("list domain collections: %v", err) + } + if notModified, err := r.onPaymentsGetStarGiftCollections(ctx, &tg.PaymentsGetStarGiftCollectionsRequest{ + Peer: &tg.InputPeerSelf{}, Hash: domain.StarGiftCollectionsHash(domainCollections), + }); err != nil { + t.Fatalf("hash list collections: %v", err) + } else if _, ok := notModified.(*tg.PaymentsStarGiftCollectionsNotModified); !ok { + t.Fatalf("hash response = %T, want not modified", notModified) + } + + update := &tg.PaymentsUpdateStarGiftCollectionRequest{Peer: &tg.InputPeerSelf{}, CollectionID: first.CollectionID} + update.SetTitle("Best") + update.SetAddStargift([]tg.InputSavedStarGiftClass{ref102}) + update.SetOrder([]tg.InputSavedStarGiftClass{ref102, ref101}) + updated, err := r.onPaymentsUpdateStarGiftCollection(ctx, update) + if err != nil { + t.Fatalf("update collection: %v", err) + } + if updated.Title != "Best" || updated.GiftsCount != 2 { + t.Fatalf("updated collection = %#v", updated) + } + + filteredReq := &tg.PaymentsGetSavedStarGiftsRequest{Peer: &tg.InputPeerSelf{}, Limit: 10} + filteredReq.SetCollectionID(first.CollectionID) + filtered, err := r.onPaymentsGetSavedStarGifts(ctx, filteredReq) + if err != nil { + t.Fatalf("filter saved gifts by collection: %v", err) + } + if filtered.Count != 2 || len(filtered.Gifts) != 2 { + t.Fatalf("filtered gifts count=%d len=%d, want 2/2", filtered.Count, len(filtered.Gifts)) + } + for _, saved := range filtered.Gifts { + ids, ok := saved.GetCollectionID() + if !ok || len(ids) == 0 || ids[0] != first.CollectionID { + t.Fatalf("saved gift collection projection = %#v", saved) + } + } + + if ok, err := r.onPaymentsToggleStarGiftsPinnedToTop(ctx, &tg.PaymentsToggleStarGiftsPinnedToTopRequest{ + Peer: &tg.InputPeerSelf{}, Stargift: []tg.InputSavedStarGiftClass{ref101, ref102}, + }); err != nil || !ok { + t.Fatalf("pin gifts = %v err %v", ok, err) + } + pinned, err := r.onPaymentsGetSavedStarGifts(ctx, &tg.PaymentsGetSavedStarGiftsRequest{Peer: &tg.InputPeerSelf{}, Limit: 10}) + if err != nil { + t.Fatalf("list pinned gifts: %v", err) + } + if len(pinned.Gifts) != 2 || !pinned.Gifts[0].PinnedToTop || !pinned.Gifts[1].PinnedToTop { + t.Fatalf("pinned projection = %#v", pinned.Gifts) + } + + if ok, err := r.onPaymentsReorderStarGiftCollections(ctx, &tg.PaymentsReorderStarGiftCollectionsRequest{ + Peer: &tg.InputPeerSelf{}, Order: []int{second.CollectionID, first.CollectionID}, + }); err != nil || !ok { + t.Fatalf("reorder collections = %v err %v", ok, err) + } + reorderedClass, err := r.onPaymentsGetStarGiftCollections(ctx, &tg.PaymentsGetStarGiftCollectionsRequest{Peer: &tg.InputPeerSelf{}}) + if err != nil { + t.Fatalf("list reordered collections: %v", err) + } + reordered := reorderedClass.(*tg.PaymentsStarGiftCollections) + if reordered.Collections[0].CollectionID != second.CollectionID { + t.Fatalf("reordered collections = %#v", reordered.Collections) + } + + if ok, err := r.onPaymentsDeleteStarGiftCollection(ctx, &tg.PaymentsDeleteStarGiftCollectionRequest{ + Peer: &tg.InputPeerSelf{}, CollectionID: first.CollectionID, + }); err != nil || !ok { + t.Fatalf("delete collection = %v err %v", ok, err) + } + afterDelete, err := r.onPaymentsGetSavedStarGifts(ctx, &tg.PaymentsGetSavedStarGiftsRequest{Peer: &tg.InputPeerSelf{}, Limit: 10}) + if err != nil { + t.Fatalf("list gifts after collection delete: %v", err) + } + for _, saved := range afterDelete.Gifts { + if ids, ok := saved.GetCollectionID(); ok { + for _, id := range ids { + if id == first.CollectionID { + t.Fatalf("deleted collection %d leaked in saved gift %#v", id, saved) + } + } + } + } +} + // 完整 star gift saga:catalog → getPaymentForm(paymentFormStarGift) → sendStarsForm(扣费+服务消息 // +paymentResult) → 收礼人 getSavedStarGifts → save/convert。 func TestStarGiftSaga(t *testing.T) { @@ -214,6 +533,21 @@ func TestStarGiftChannelSaga(t *testing.T) { channel := created.Channel channelPeer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash} channelInput := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash} + giftService := r.deps.Gifts.(*appstargifts.Service) + if _, err := giftService.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{ + GiftID: gift.ID, UpgradeStars: 75, SupplyTotal: 10, SlugPrefix: "channel-cake", + Models: []domain.StarGiftCollectibleAttribute{collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8201, "Aurora")}, + Patterns: []domain.StarGiftCollectibleAttribute{collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8202, "Orbit")}, + Backdrops: []domain.StarGiftCollectibleAttribute{collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 2, "Midnight")}, + Actor: "test", CommandID: "channel-collectible-rpc", + }); err != nil { + t.Fatalf("publish channel collectible pool: %v", err) + } + if _, err := r.onPaymentsGetPaymentForm(senderCtx, &tg.PaymentsGetPaymentFormRequest{Invoice: &tg.InputInvoiceStarGift{ + Peer: channelPeer, GiftID: gift.ID, IncludeUpgrade: true, + }}); err == nil { + t.Fatal("channel include_upgrade must be rejected while channel upgrade is blocked") + } inv := &tg.InputInvoiceStarGift{ Peer: channelPeer, GiftID: gift.ID, @@ -265,6 +599,9 @@ func TestStarGiftChannelSaga(t *testing.T) { if savedRes.Count != 1 || len(savedRes.Gifts) != 1 { t.Fatalf("channel saved gifts = count %d len %d, want 1/1", savedRes.Count, len(savedRes.Gifts)) } + if savedRes.Gifts[0].CanUpgrade { + t.Fatal("channel saved gift must not advertise upgrade while channel aggregate is blocked") + } savedID, ok := savedRes.Gifts[0].GetSavedID() if !ok || savedID <= 0 { t.Fatalf("saved gift saved_id = %d ok %v, want positive", savedID, ok) @@ -366,17 +703,20 @@ func TestStarGiftInsufficientBalance(t *testing.T) { msgStore := memory.NewMessageStore(dialogs) sender, _ := users.Create(ctx, domain.User{AccessHash: 7201, Phone: "15550007201", FirstName: "Poor"}) recipient, _ := users.Create(ctx, domain.User{AccessHash: 7202, Phone: "15550007202", FirstName: "Rich"}) - gift := domain.StarGift{ID: 8002, Stars: 5000, ConvertStars: 5000, Title: "Expensive", - Sticker: domain.Document{ID: 701, AccessHash: 7, DCID: 2, MimeType: "image/webp"}} + gift := domain.StarGift{ID: 8002, RevisionID: 9002, Stars: 5000, ConvertStars: 5000, Title: "Expensive", + Sticker: domain.Document{ID: 701, AccessHash: 7, DCID: 2, MimeType: "application/x-tgsticker", Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}}} + giftStore := memory.NewStarGiftStore() + giftStore.SeedCatalog([]domain.StarGift{gift}) r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{ Users: appusers.NewService(users), Messages: appmessages.NewService(msgStore, dialogs), Stars: appstars.NewService(memory.NewStarsStore(), appstars.WithStartingGrant(1000)), // < 5000 - Gifts: appstargifts.NewService(memory.NewStarGiftStore(), stubGiftCatalog{[]domain.StarGift{gift}}), + Gifts: appstargifts.NewService(giftStore, nil, 2), }, zaptest.NewLogger(t), clock.System) senderCtx := WithUserID(ctx, sender.ID) inv := &tg.InputInvoiceStarGift{Peer: &tg.InputPeerUser{UserID: recipient.ID, AccessHash: recipient.AccessHash}, GiftID: gift.ID} - if _, err := r.onPaymentsSendStarsForm(senderCtx, &tg.PaymentsSendStarsFormRequest{Invoice: inv}); err == nil { + peer := domain.Peer{Type: domain.PeerTypeUser, ID: recipient.ID} + if _, err := r.onPaymentsSendStarsForm(senderCtx, &tg.PaymentsSendStarsFormRequest{FormID: starGiftFormID(sender.ID, peer, gift), Invoice: inv}); err == nil { t.Fatalf("over-budget gift should error BALANCE_TOO_LOW") } // 余额未变。 @@ -385,6 +725,25 @@ func TestStarGiftInsufficientBalance(t *testing.T) { } } +func TestStarGiftFormBindsCatalogRevisionAndPrice(t *testing.T) { + r, sender, recipient, gift := starGiftTestRouter(t) + ctx := WithUserID(context.Background(), sender.ID) + peer := domain.Peer{Type: domain.PeerTypeUser, ID: recipient.ID} + base := starGiftFormID(sender.ID, peer, gift) + changedRevision := gift + changedRevision.RevisionID++ + changedPrice := gift + changedPrice.Stars++ + changedPeer := domain.Peer{Type: domain.PeerTypeUser, ID: recipient.ID + 1} + if base == starGiftFormID(sender.ID, peer, changedRevision) || base == starGiftFormID(sender.ID, peer, changedPrice) || base == starGiftFormID(sender.ID, changedPeer, gift) { + t.Fatal("star gift form id must bind revision, price and recipient") + } + inv := &tg.InputInvoiceStarGift{Peer: &tg.InputPeerUser{UserID: recipient.ID, AccessHash: recipient.AccessHash}, GiftID: gift.ID} + if _, err := r.onPaymentsSendStarsForm(ctx, &tg.PaymentsSendStarsFormRequest{FormID: base + 1, Invoice: inv}); !tgerr.Is(err, "STARS_FORM_AMOUNT_MISMATCH") { + t.Fatalf("bad form err=%v", err) + } +} + func TestStarsTopupInvoiceFallbackCreditsBalance(t *testing.T) { r, sender, _, _ := starGiftTestRouter(t) ctx := context.Background() diff --git a/internal/store/memory/star_gift.go b/internal/store/memory/star_gift.go index 0ef0b47b..b6ab41f6 100644 --- a/internal/store/memory/star_gift.go +++ b/internal/store/memory/star_gift.go @@ -3,6 +3,7 @@ package memory import ( "context" "sort" + "strings" "sync" "telesrv/internal/domain" @@ -10,14 +11,256 @@ import ( // StarGiftStore 是 store.StarGiftStore 的内存实现。 type StarGiftStore struct { - mu sync.Mutex - nextID int64 - gifts []domain.SavedStarGift // 追加序 + mu sync.Mutex + nextID int64 + nextGiftID int64 + nextRevID int64 + gifts []domain.SavedStarGift // 追加序 + catalog map[int64]domain.StarGift + revisions map[int64]domain.StarGift + enabled map[int64]bool + sortOrder map[int64]int + animations map[int64][]byte + collectibles map[int64]domain.StarGiftCollectibleRevision + uniqueByID map[int64]domain.UniqueStarGift + uniqueBySlug map[string]int64 + collections map[domain.Peer][]domain.StarGiftCollection + nextAttributeID int64 + nextCollectionID int } // NewStarGiftStore 创建内存 StarGiftStore。 func NewStarGiftStore() *StarGiftStore { - return &StarGiftStore{} + return &StarGiftStore{ + catalog: make(map[int64]domain.StarGift), revisions: make(map[int64]domain.StarGift), + enabled: make(map[int64]bool), sortOrder: make(map[int64]int), animations: make(map[int64][]byte), + collectibles: make(map[int64]domain.StarGiftCollectibleRevision), + uniqueByID: make(map[int64]domain.UniqueStarGift), uniqueBySlug: make(map[string]int64), + collections: make(map[domain.Peer][]domain.StarGiftCollection), + } +} + +// SeedCatalog installs valid immutable catalog snapshots for tests. +func (s *StarGiftStore) SeedCatalog(gifts []domain.StarGift) { + s.mu.Lock() + defer s.mu.Unlock() + for _, gift := range gifts { + if gift.RevisionID == 0 { + s.nextRevID++ + gift.RevisionID = s.nextRevID + } + if gift.ID > s.nextGiftID { + s.nextGiftID = gift.ID + } + if gift.RevisionID > s.nextRevID { + s.nextRevID = gift.RevisionID + } + s.catalog[gift.ID] = gift + s.revisions[gift.RevisionID] = gift + s.enabled[gift.ID] = true + } +} + +func (s *StarGiftStore) Catalog(_ context.Context) ([]domain.StarGift, error) { + s.mu.Lock() + defer s.mu.Unlock() + out := make([]domain.StarGift, 0, len(s.catalog)) + for id, gift := range s.catalog { + if s.enabled[id] { + out = append(out, gift) + } + } + sort.Slice(out, func(i, j int) bool { + if s.sortOrder[out[i].ID] == s.sortOrder[out[j].ID] { + return out[i].ID < out[j].ID + } + return s.sortOrder[out[i].ID] < s.sortOrder[out[j].ID] + }) + return out, nil +} + +func (s *StarGiftStore) CatalogGift(_ context.Context, giftID int64) (domain.StarGift, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + gift, ok := s.catalog[giftID] + return gift, ok && s.enabled[giftID], nil +} + +func (s *StarGiftStore) CatalogRevision(_ context.Context, revisionID int64) (domain.StarGift, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + gift, ok := s.revisions[revisionID] + return gift, ok, nil +} + +func (s *StarGiftStore) CreateCatalogRevision(_ context.Context, write domain.StarGiftCatalogWrite) (domain.StarGiftCatalogEntry, error) { + s.mu.Lock() + defer s.mu.Unlock() + giftID := write.GiftID + if giftID == 0 { + s.nextGiftID++ + giftID = s.nextGiftID + } else if _, ok := s.catalog[giftID]; !ok { + return domain.StarGiftCatalogEntry{}, domain.ErrStarGiftNotFound + } + s.nextRevID++ + gift := domain.StarGift{ID: giftID, RevisionID: s.nextRevID, Stars: write.Stars, ConvertStars: write.ConvertStars, Title: write.Title, Sticker: write.Document} + s.catalog[giftID] = gift + s.revisions[gift.RevisionID] = gift + s.enabled[giftID] = write.Enabled + s.sortOrder[giftID] = write.SortOrder + s.animations[giftID] = append([]byte(nil), write.Animation.JSON...) + return domain.StarGiftCatalogEntry{Gift: gift, Enabled: write.Enabled, SortOrder: write.SortOrder}, nil +} + +func (s *StarGiftStore) SetCatalogEnabled(_ context.Context, giftID int64, enabled bool) (bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.catalog[giftID]; !ok { + return false, domain.ErrStarGiftNotFound + } + changed := s.enabled[giftID] != enabled + s.enabled[giftID] = enabled + return changed, nil +} + +func (s *StarGiftStore) SetCatalogSortOrder(_ context.Context, giftID int64, sortOrder int) (bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.catalog[giftID]; !ok { + return false, domain.ErrStarGiftNotFound + } + changed := s.sortOrder[giftID] != sortOrder + s.sortOrder[giftID] = sortOrder + return changed, nil +} + +func (s *StarGiftStore) AnimationJSON(_ context.Context, giftID int64) ([]byte, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + raw, ok := s.animations[giftID] + return append([]byte(nil), raw...), ok, nil +} + +func (s *StarGiftStore) PublishCollectibleRevision(_ context.Context, write domain.StarGiftCollectibleWrite) (domain.StarGiftCollectibleRevision, error) { + if err := domain.ValidateStarGiftCollectibleWrite(write); err != nil { + return domain.StarGiftCollectibleRevision{}, err + } + s.mu.Lock() + defer s.mu.Unlock() + if _, ok := s.catalog[write.GiftID]; !ok { + return domain.StarGiftCollectibleRevision{}, domain.ErrStarGiftNotFound + } + previous := s.collectibles[write.GiftID] + revision := domain.StarGiftCollectibleRevision{ + ID: previous.ID + 1, GiftID: write.GiftID, Revision: previous.Revision + 1, + UpgradeStars: write.UpgradeStars, SupplyTotal: write.SupplyTotal, + SlugPrefix: strings.ToLower(strings.TrimSpace(write.SlugPrefix)), Published: true, + CreatedBy: write.Actor, + } + if revision.ID == 1 { + revision.ID = write.GiftID*1000 + 1 + } + revision.Models = s.allocateCollectibleAttributes(write.Models, revision.ID) + revision.Patterns = s.allocateCollectibleAttributes(write.Patterns, revision.ID) + revision.Backdrops = s.allocateCollectibleAttributes(write.Backdrops, revision.ID) + s.collectibles[write.GiftID] = revision + gift := s.catalog[write.GiftID] + gift.UpgradeStars = revision.UpgradeStars + gift.UpgradeTotal = revision.SupplyTotal + gift.UpgradeIssued = revision.Issued + s.catalog[write.GiftID] = gift + return cloneCollectibleRevision(revision), nil +} + +func (s *StarGiftStore) allocateCollectibleAttributes(in []domain.StarGiftCollectibleAttribute, revisionID int64) []domain.StarGiftCollectibleAttribute { + out := make([]domain.StarGiftCollectibleAttribute, len(in)) + for i, attribute := range in { + s.nextAttributeID++ + attribute.ID = s.nextAttributeID + attribute.CollectibleRevisionID = revisionID + out[i] = cloneCollectibleAttribute(attribute) + } + return out +} + +func (s *StarGiftStore) ActiveCollectibleRevision(_ context.Context, giftID int64) (domain.StarGiftCollectibleRevision, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + revision, ok := s.collectibles[giftID] + return cloneCollectibleRevision(revision), ok, nil +} + +func (s *StarGiftStore) CollectibleAvailability(_ context.Context, giftIDs []int64) (map[int64]domain.StarGiftCollectibleAvailability, error) { + s.mu.Lock() + defer s.mu.Unlock() + out := make(map[int64]domain.StarGiftCollectibleAvailability, len(giftIDs)) + for _, giftID := range giftIDs { + revision, ok := s.collectibles[giftID] + if !ok || !revision.Published { + continue + } + out[giftID] = domain.StarGiftCollectibleAvailability{ + UpgradeStars: revision.UpgradeStars, + SupplyTotal: revision.SupplyTotal, + Issued: revision.Issued, + } + } + return out, nil +} + +func (s *StarGiftStore) CollectibleAnimationJSON(_ context.Context, giftID int64, kind domain.StarGiftCollectibleAttributeKind, attributeID int64) ([]byte, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + revision, ok := s.collectibles[giftID] + if !ok { + return nil, false, nil + } + var attributes []domain.StarGiftCollectibleAttribute + switch kind { + case domain.StarGiftCollectibleModel: + attributes = revision.Models + case domain.StarGiftCollectiblePattern: + attributes = revision.Patterns + default: + return nil, false, nil + } + for _, attribute := range attributes { + if attribute.ID == attributeID && attribute.Animation != nil { + return append([]byte(nil), attribute.Animation.JSON...), true, nil + } + } + return nil, false, nil +} + +func (s *StarGiftStore) UniqueBySlug(_ context.Context, slug string) (domain.UniqueStarGift, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + id, ok := s.uniqueBySlug[strings.ToLower(strings.TrimSpace(slug))] + if !ok { + return domain.UniqueStarGift{}, false, nil + } + unique, ok := s.uniqueByID[id] + return unique, ok, nil +} + +func (s *StarGiftStore) UniqueByID(_ context.Context, uniqueGiftID int64) (domain.UniqueStarGift, bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + unique, ok := s.uniqueByID[uniqueGiftID] + return unique, ok, nil +} + +func (s *StarGiftStore) UniqueByIDs(_ context.Context, uniqueGiftIDs []int64) (map[int64]domain.UniqueStarGift, error) { + s.mu.Lock() + defer s.mu.Unlock() + out := make(map[int64]domain.UniqueStarGift, len(uniqueGiftIDs)) + for _, id := range uniqueGiftIDs { + if gift, ok := s.uniqueByID[id]; ok { + out[id] = gift + } + } + return out, nil } func (s *StarGiftStore) Create(_ context.Context, gift domain.SavedStarGift) (int64, error) { @@ -37,6 +280,13 @@ func (s *StarGiftStore) Create(_ context.Context, gift domain.SavedStarGift) (in } func (s *StarGiftStore) ListByOwner(_ context.Context, owner domain.Peer, excludeUnsaved bool, offset string, limit int) (domain.SavedStarGiftPage, error) { + return s.ListByOwnerFiltered(context.Background(), domain.SavedStarGiftFilter{ + Owner: owner, ExcludeUnsaved: excludeUnsaved, Offset: offset, Limit: limit, + }) +} + +func (s *StarGiftStore) ListByOwnerFiltered(_ context.Context, filter domain.SavedStarGiftFilter) (domain.SavedStarGiftPage, error) { + owner, offset, limit := filter.Owner, filter.Offset, filter.Limit if !validStarGiftOwner(owner) { return domain.SavedStarGiftPage{}, nil } @@ -50,7 +300,31 @@ func (s *StarGiftStore) ListByOwner(_ context.Context, owner domain.Peer, exclud if g.Owner != owner || g.Converted { continue } - if excludeUnsaved && g.Unsaved { + if filter.ExcludeUnsaved && g.Unsaved { + continue + } + if filter.ExcludeSaved && !g.Unsaved { + continue + } + if filter.ExcludeUnique && g.UniqueGiftID != 0 { + continue + } + if filter.ExcludeUnlimited && g.UniqueGiftID == 0 { + continue + } + upgradable := false + if g.UniqueGiftID == 0 { + if gift, ok := s.catalog[g.GiftID]; ok { + upgradable = gift.UpgradeStars > 0 && gift.UpgradeIssued < gift.UpgradeTotal + } + } + if filter.ExcludeUpgradable && upgradable { + continue + } + if filter.ExcludeUnupgradable && !upgradable { + continue + } + if filter.CollectionID > 0 && !containsInt(g.CollectionIDs, filter.CollectionID) { continue } matched = append(matched, g) @@ -82,6 +356,37 @@ func (s *StarGiftStore) ListByOwner(_ context.Context, owner domain.Peer, exclud return page, nil } +func (s *StarGiftStore) ResolveSavedIDs(_ context.Context, owner domain.Peer, refs []domain.SavedStarGiftRef) ([]int64, error) { + if !validStarGiftOwner(owner) || len(refs) > domain.MaxStarGiftCollectionItems { + return nil, domain.ErrStarGiftCollectibleInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + out := make([]int64, 0, len(refs)) + seen := make(map[int64]struct{}, len(refs)) + for _, ref := range refs { + if ref.Owner != owner || !ref.Valid() { + return nil, domain.ErrStarGiftNotFound + } + var id int64 + for _, gift := range s.gifts { + if savedStarGiftMatchesRef(gift, ref) && !gift.Converted { + id = gift.ID + break + } + } + if id == 0 { + return nil, domain.ErrStarGiftNotFound + } + if _, exists := seen[id]; exists { + return nil, domain.ErrStarGiftCollectibleInvalid + } + seen[id] = struct{}{} + out = append(out, id) + } + return out, nil +} + func (s *StarGiftStore) GetByRef(_ context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, bool, error) { if !ref.Valid() { return domain.SavedStarGift{}, false, nil @@ -134,19 +439,298 @@ func (s *StarGiftStore) MarkConverted(_ context.Context, ref domain.SavedStarGif defer s.mu.Unlock() for i := range s.gifts { if savedStarGiftMatchesRef(s.gifts[i], ref) { + if s.gifts[i].UniqueGiftID != 0 { + return domain.SavedStarGift{}, domain.ErrStarGiftAlreadyUpgraded + } if s.gifts[i].Converted { return domain.SavedStarGift{}, domain.ErrStarGiftAlreadyConverted } s.gifts[i].Converted = true s.gifts[i].Unsaved = true + s.gifts[i].PinnedOrder = 0 + for collectionIndex := range s.collections[ref.Owner] { + collection := &s.collections[ref.Owner][collectionIndex] + next := collection.GiftIDs[:0] + for _, giftID := range collection.GiftIDs { + if giftID != s.gifts[i].ID { + next = append(next, giftID) + } + } + collection.GiftIDs = next + collection.Hash = domain.StarGiftCollectionHash(collection.Title, collection.GiftIDs) + } + s.refreshCollectionMembershipsLocked(ref.Owner) return s.gifts[i], nil } } return domain.SavedStarGift{}, domain.ErrStarGiftNotFound } +func (s *StarGiftStore) ListCollections(_ context.Context, owner domain.Peer) ([]domain.StarGiftCollection, error) { + s.mu.Lock() + defer s.mu.Unlock() + return cloneStarGiftCollections(s.collections[owner]), nil +} + +func (s *StarGiftStore) CreateCollection(_ context.Context, owner domain.Peer, title string, savedGiftIDs []int64) (domain.StarGiftCollection, error) { + title = strings.TrimSpace(title) + if !validStarGiftOwner(owner) || title == "" || len([]rune(title)) > domain.MaxStarGiftCollectionTitleRunes { + return domain.StarGiftCollection{}, domain.ErrStarGiftCollectibleInvalid + } + s.mu.Lock() + defer s.mu.Unlock() + if len(s.collections[owner]) >= domain.MaxStarGiftCollectionsPerPeer { + return domain.StarGiftCollection{}, domain.ErrStarGiftCollectionsFull + } + ids, err := s.validCollectionGiftIDsLocked(owner, savedGiftIDs) + if err != nil { + return domain.StarGiftCollection{}, err + } + s.nextCollectionID++ + collection := domain.StarGiftCollection{Owner: owner, CollectionID: s.nextCollectionID, Title: title, GiftIDs: ids, SortOrder: len(s.collections[owner])} + collection.Hash = domain.StarGiftCollectionHash(collection.Title, collection.GiftIDs) + s.collections[owner] = append(s.collections[owner], collection) + s.refreshCollectionMembershipsLocked(owner) + return collection, nil +} + +func (s *StarGiftStore) UpdateCollection(_ context.Context, owner domain.Peer, collectionID int, patch domain.StarGiftCollectionPatch) (domain.StarGiftCollection, error) { + s.mu.Lock() + defer s.mu.Unlock() + collections := s.collections[owner] + index := -1 + for i := range collections { + if collections[i].CollectionID == collectionID { + index = i + break + } + } + if index < 0 { + return domain.StarGiftCollection{}, domain.ErrStarGiftCollectionNotFound + } + collection := collections[index] + if patch.Title != nil { + title := strings.TrimSpace(*patch.Title) + if title == "" || len([]rune(title)) > domain.MaxStarGiftCollectionTitleRunes { + return domain.StarGiftCollection{}, domain.ErrStarGiftCollectibleInvalid + } + collection.Title = title + } + deleteSet := make(map[int64]struct{}, len(patch.DeleteIDs)) + for _, id := range patch.DeleteIDs { + deleteSet[id] = struct{}{} + } + next := make([]int64, 0, len(collection.GiftIDs)+len(patch.AddIDs)) + for _, id := range collection.GiftIDs { + if _, deleted := deleteSet[id]; !deleted { + next = append(next, id) + } + } + add, err := s.validCollectionGiftIDsLocked(owner, patch.AddIDs) + if err != nil { + return domain.StarGiftCollection{}, err + } + next = appendUniqueInt64(next, add...) + if patch.Order != nil { + order, err := s.validCollectionGiftIDsLocked(owner, patch.Order) + if err != nil || !sameInt64Set(order, next) { + return domain.StarGiftCollection{}, domain.ErrStarGiftCollectibleInvalid + } + next = order + } + if len(next) > domain.MaxStarGiftCollectionItems { + return domain.StarGiftCollection{}, domain.ErrStarGiftCollectibleInvalid + } + collection.GiftIDs = next + collection.Hash = domain.StarGiftCollectionHash(collection.Title, collection.GiftIDs) + collections[index] = collection + s.collections[owner] = collections + s.refreshCollectionMembershipsLocked(owner) + return collection, nil +} + +func (s *StarGiftStore) DeleteCollection(_ context.Context, owner domain.Peer, collectionID int) (bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + collections := s.collections[owner] + for i := range collections { + if collections[i].CollectionID == collectionID { + collections = append(collections[:i], collections[i+1:]...) + for j := range collections { + collections[j].SortOrder = j + } + s.collections[owner] = collections + s.refreshCollectionMembershipsLocked(owner) + return true, nil + } + } + return false, nil +} + +func (s *StarGiftStore) ReorderCollections(_ context.Context, owner domain.Peer, collectionIDs []int) error { + s.mu.Lock() + defer s.mu.Unlock() + collections := s.collections[owner] + if len(collectionIDs) != len(collections) { + return domain.ErrStarGiftCollectibleInvalid + } + byID := make(map[int]domain.StarGiftCollection, len(collections)) + for _, collection := range collections { + byID[collection.CollectionID] = collection + } + next := make([]domain.StarGiftCollection, 0, len(collections)) + for order, id := range collectionIDs { + collection, ok := byID[id] + if !ok { + return domain.ErrStarGiftCollectibleInvalid + } + delete(byID, id) + collection.SortOrder = order + next = append(next, collection) + } + s.collections[owner] = next + return nil +} + +func (s *StarGiftStore) SetPinned(_ context.Context, owner domain.Peer, savedGiftIDs []int64) error { + s.mu.Lock() + defer s.mu.Unlock() + ids, err := s.validCollectionGiftIDsLocked(owner, savedGiftIDs) + if err != nil { + return err + } + order := make(map[int64]int, len(ids)) + for i, id := range ids { + order[id] = i + 1 + } + for i := range s.gifts { + if s.gifts[i].Owner == owner { + s.gifts[i].PinnedOrder = order[s.gifts[i].ID] + } + } + return nil +} + +// refreshCollectionMembershipsLocked keeps the in-memory saved-gift projection +// equivalent to the PostgreSQL join projection. Callers must hold s.mu. +func (s *StarGiftStore) refreshCollectionMembershipsLocked(owner domain.Peer) { + memberships := make(map[int64][]int) + for _, collection := range s.collections[owner] { + for _, giftID := range collection.GiftIDs { + memberships[giftID] = append(memberships[giftID], collection.CollectionID) + } + } + for i := range s.gifts { + if s.gifts[i].Owner != owner { + continue + } + s.gifts[i].CollectionIDs = append([]int(nil), memberships[s.gifts[i].ID]...) + } +} + +func (s *StarGiftStore) validCollectionGiftIDsLocked(owner domain.Peer, ids []int64) ([]int64, error) { + if len(ids) > domain.MaxStarGiftCollectionItems { + return nil, domain.ErrStarGiftCollectibleInvalid + } + out := make([]int64, 0, len(ids)) + seen := make(map[int64]struct{}, len(ids)) + for _, id := range ids { + if _, ok := seen[id]; ok { + continue + } + valid := false + for _, gift := range s.gifts { + if gift.ID == id && gift.Owner == owner && !gift.Converted { + valid = true + break + } + } + if !valid { + return nil, domain.ErrStarGiftNotFound + } + seen[id] = struct{}{} + out = append(out, id) + } + return out, nil +} + +func appendUniqueInt64(dst []int64, values ...int64) []int64 { + seen := make(map[int64]struct{}, len(dst)+len(values)) + for _, id := range dst { + seen[id] = struct{}{} + } + for _, id := range values { + if _, ok := seen[id]; !ok { + seen[id] = struct{}{} + dst = append(dst, id) + } + } + return dst +} + +func sameInt64Set(a, b []int64) bool { + if len(a) != len(b) { + return false + } + seen := make(map[int64]int, len(a)) + for _, id := range a { + seen[id]++ + } + for _, id := range b { + seen[id]-- + if seen[id] < 0 { + return false + } + } + return true +} + +func cloneCollectibleAttribute(in domain.StarGiftCollectibleAttribute) domain.StarGiftCollectibleAttribute { + out := in + if in.Document != nil { + document := *in.Document + out.Document = &document + } + if in.Animation != nil { + animation := *in.Animation + animation.JSON = append([]byte(nil), in.Animation.JSON...) + animation.TGS = append([]byte(nil), in.Animation.TGS...) + animation.SHA256 = append([]byte(nil), in.Animation.SHA256...) + out.Animation = &animation + } + if in.Blob != nil { + blob := *in.Blob + out.Blob = &blob + } + return out +} + +func cloneCollectibleRevision(in domain.StarGiftCollectibleRevision) domain.StarGiftCollectibleRevision { + out := in + clone := func(attributes []domain.StarGiftCollectibleAttribute) []domain.StarGiftCollectibleAttribute { + copy := make([]domain.StarGiftCollectibleAttribute, len(attributes)) + for i, attribute := range attributes { + copy[i] = cloneCollectibleAttribute(attribute) + } + return copy + } + out.Models = clone(in.Models) + out.Patterns = clone(in.Patterns) + out.Backdrops = clone(in.Backdrops) + return out +} + +func cloneStarGiftCollections(in []domain.StarGiftCollection) []domain.StarGiftCollection { + out := make([]domain.StarGiftCollection, len(in)) + for i, collection := range in { + out[i] = collection + out[i].GiftIDs = append([]int64(nil), collection.GiftIDs...) + } + return out +} + func validSavedStarGift(g domain.SavedStarGift) bool { - if g.GiftID == 0 || !validStarGiftOwner(g.Owner) { + if g.GiftID == 0 || g.RevisionID == 0 || !validStarGiftOwner(g.Owner) { return false } switch g.Owner.Type { diff --git a/internal/store/postgres/authkey_integration_test.go b/internal/store/postgres/authkey_integration_test.go index f3c7b86b..b5a7071e 100644 --- a/internal/store/postgres/authkey_integration_test.go +++ b/internal/store/postgres/authkey_integration_test.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "errors" "os" + "strings" "testing" "github.com/jackc/pgx/v5/pgxpool" @@ -19,6 +20,13 @@ func testPool(t *testing.T) *pgxpool.Pool { 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 := Migrate(dsn); err != nil { t.Fatalf("migrate: %v", err) } diff --git a/internal/store/postgres/message_send.go b/internal/store/postgres/message_send.go index 500f74f7..beb5c9c8 100644 --- a/internal/store/postgres/message_send.go +++ b/internal/store/postgres/message_send.go @@ -76,8 +76,17 @@ ON CONFLICT (id) DO NOTHING } func (s *MessageStore) SendPrivateText(ctx context.Context, req domain.SendPrivateTextRequest) (res domain.SendPrivateTextResult, err error) { + return s.sendPrivateTextWithHooks(ctx, req, privateSendTxHooks{}) +} + +type privateSendTxHooks struct { + before func(context.Context, pgx.Tx, *domain.SendPrivateTextRequest) error + after func(context.Context, pgx.Tx, domain.SendPrivateTextResult) error +} + +func (s *MessageStore) sendPrivateTextWithHooks(ctx context.Context, req domain.SendPrivateTextRequest, hooks privateSendTxHooks) (res domain.SendPrivateTextResult, err error) { for attempt := 0; attempt < 2; attempt++ { - res, err = s.sendPrivateTextOnce(ctx, req) + res, err = s.sendPrivateTextOnce(ctx, req, hooks) if err == nil { return res, nil } @@ -91,7 +100,7 @@ func (s *MessageStore) SendPrivateText(ctx context.Context, req domain.SendPriva return domain.SendPrivateTextResult{}, err } -func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendPrivateTextRequest) (res domain.SendPrivateTextResult, err error) { +func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendPrivateTextRequest, hooks privateSendTxHooks) (res domain.SendPrivateTextResult, err error) { if req.SenderUserID == 0 || req.RecipientUserID == 0 { return domain.SendPrivateTextResult{}, fmt.Errorf("send private text: missing user id") } @@ -108,10 +117,6 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP if err != nil { return domain.SendPrivateTextResult{}, err } - mediaJSON, err := encodeMessageMedia(req.Media) - if err != nil { - return domain.SendPrivateTextResult{}, err - } // reply_markup(bot inline keyboard)随消息一并入双盒;普通用户发送恒 nil → "{}"。 replyMarkupJSON, err := encodeReplyMarkup(req.ReplyMarkup) if err != nil { @@ -181,6 +186,15 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP if err := lockUsersForUpdate(ctx, tx, req.SenderUserID, req.RecipientUserID); err != nil { return domain.SendPrivateTextResult{}, fmt.Errorf("lock send users: %w", err) } + if hooks.before != nil { + if err := hooks.before(ctx, tx, &req); err != nil { + return domain.SendPrivateTextResult{}, err + } + } + mediaJSON, err := encodeMessageMedia(req.Media) + if err != nil { + return domain.SendPrivateTextResult{}, err + } ttlPeriod := req.TTLPeriod if ttlPeriod == 0 { ttlPeriod, err = privateHistoryTTLPeriod(ctx, tx, req.SenderUserID, req.RecipientUserID) @@ -298,12 +312,21 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP if err := appendNewMessageEvent(ctx, qtx, sender); err != nil { return domain.SendPrivateTextResult{}, err } + originUserID := req.OriginUserID + if originUserID == 0 { + originUserID = req.SenderUserID + } + senderExcludeAuthKeyID, senderExcludeSessionID := int64(0), int64(0) + if originUserID == req.SenderUserID { + senderExcludeAuthKeyID = authKeyIDToInt64(req.OriginAuthKeyID) + senderExcludeSessionID = req.OriginSessionID + } if err := enqueueDispatch(ctx, qtx, sqlcgen.EnqueueDispatchParams{ TargetUserID: req.SenderUserID, Pts: int32(senderPts), EventType: string(domain.UpdateEventNewMessage), - ExcludeAuthKeyID: authKeyIDToInt64(req.OriginAuthKeyID), - ExcludeSessionID: req.OriginSessionID, + ExcludeAuthKeyID: senderExcludeAuthKeyID, + ExcludeSessionID: senderExcludeSessionID, }); err != nil { return domain.SendPrivateTextResult{}, fmt.Errorf("enqueue sender dispatch: %w", err) } @@ -360,12 +383,17 @@ func (s *MessageStore) sendPrivateTextOnce(ctx context.Context, req domain.SendP if err := appendNewMessageEvent(ctx, qtx, recipient); err != nil { return domain.SendPrivateTextResult{}, err } + recipientExcludeAuthKeyID, recipientExcludeSessionID := int64(0), int64(0) + if originUserID == req.RecipientUserID { + recipientExcludeAuthKeyID = authKeyIDToInt64(req.OriginAuthKeyID) + recipientExcludeSessionID = req.OriginSessionID + } if err := enqueueDispatch(ctx, qtx, sqlcgen.EnqueueDispatchParams{ TargetUserID: req.RecipientUserID, Pts: int32(recipientPts), EventType: string(domain.UpdateEventNewMessage), - ExcludeAuthKeyID: 0, - ExcludeSessionID: 0, + ExcludeAuthKeyID: recipientExcludeAuthKeyID, + ExcludeSessionID: recipientExcludeSessionID, }); err != nil { return domain.SendPrivateTextResult{}, fmt.Errorf("enqueue recipient dispatch: %w", err) } @@ -397,17 +425,23 @@ WHERE sender_user_id = $1 if tag.RowsAffected() != 1 { return domain.SendPrivateTextResult{}, fmt.Errorf("save private send receipt: private message %d already has or lost its immutable receipt", pm.ID) } + result := domain.SendPrivateTextResult{ + SenderMessage: sender, + RecipientMessage: recipient, + SenderEvent: eventFromMessage(sender), + RecipientEvent: eventFromMessage(recipient), + } + if hooks.after != nil { + if err := hooks.after(ctx, tx, result); err != nil { + return domain.SendPrivateTextResult{}, err + } + } if err := tx.Commit(ctx); err != nil { return domain.SendPrivateTextResult{}, fmt.Errorf("commit send message tx: %w", err) } committed = true - return domain.SendPrivateTextResult{ - SenderMessage: sender, - RecipientMessage: recipient, - SenderEvent: eventFromMessage(sender), - RecipientEvent: eventFromMessage(recipient), - }, nil + return result, nil } // LookupPrivateSendReplay reads an existing receipt without permission checks, source/media diff --git a/internal/store/postgres/read_model_listener.go b/internal/store/postgres/read_model_listener.go index 268deed9..0bf27c2a 100644 --- a/internal/store/postgres/read_model_listener.go +++ b/internal/store/postgres/read_model_listener.go @@ -33,6 +33,12 @@ type ReadModelCacheSet struct { RPCProjections RPCProjectionReadModelCache BaseUsers BaseUserCache BotProfiles BotProfileReadModelCache + StarGifts StarGiftCatalogCache +} + +type StarGiftCatalogCache interface { + InvalidateStarGiftCatalog() + FlushStarGiftCatalog() } // BaseUserCache 是跨进程共享的 user:base 缓存(Redis)。user_base read-model 事件必须删除 @@ -214,7 +220,8 @@ func (l *ReadModelChangeListener) empty() bool { l.caches.PrivateMediaCounts == nil && l.caches.RPCProjections == nil && l.caches.BaseUsers == nil && - l.caches.BotProfiles == nil + l.caches.BotProfiles == nil && + l.caches.StarGifts == nil } func (l *ReadModelChangeListener) flush(reasons ...string) { @@ -287,6 +294,10 @@ func (l *ReadModelChangeListener) flush(reasons ...string) { l.caches.BotProfiles.FlushBotProfileReadModel() flushed = append(flushed, "bot_profiles") } + if l.caches.StarGifts != nil { + l.caches.StarGifts.FlushStarGiftCatalog() + flushed = append(flushed, "star_gifts") + } // 注意:BaseUsers(Redis) 刻意不在重连时 flush——它是跨实例共享缓存,整库清空会误伤 // 其它实例;漏掉的通知由其 5min TTL 兜底。 l.log.Info("read model caches flushed", @@ -315,6 +326,10 @@ func (l *ReadModelChangeListener) handlePayload(payload string) { } } switch evt.Model { + case "star_gift_catalog": + if l.caches.StarGifts != nil { + l.caches.StarGifts.InvalidateStarGiftCatalog() + } case "user_base": if evt.PeerType == "user" && evt.PeerID != 0 { if l.caches.RPCProjections != nil { diff --git a/internal/store/postgres/star_gift.go b/internal/store/postgres/star_gift.go index 44d142e6..f334c676 100644 --- a/internal/store/postgres/star_gift.go +++ b/internal/store/postgres/star_gift.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "github.com/jackc/pgx/v5" @@ -21,6 +22,290 @@ func NewStarGiftStore(db sqlcgen.DBTX) *StarGiftStore { return &StarGiftStore{db: db} } +const starGiftCatalogSelect = ` +SELECT c.gift_id, r.id, r.stars, r.convert_stars, r.title, + COALESCE(cr.upgrade_stars, 0), COALESCE(cr.supply_total, 0), COALESCE(cr.issued, 0), + d.id, d.access_hash, d.file_reference, d.date, d.mime_type, d.size, d.dc_id, + d.attributes::text, d.thumbs::text +FROM star_gift_catalog c +JOIN star_gift_catalog_revisions r ON r.id = c.active_revision_id +LEFT JOIN star_gift_collectible_revisions cr ON cr.id = c.collectible_revision_id AND cr.status = 'published' +JOIN documents d ON d.id = r.document_id` + +func (s *StarGiftStore) Catalog(ctx context.Context) ([]domain.StarGift, error) { + rows, err := s.db.Query(ctx, starGiftCatalogSelect+` +WHERE c.enabled +ORDER BY c.sort_order, c.gift_id`) + if err != nil { + return nil, fmt.Errorf("list star gift catalog: %w", err) + } + defer rows.Close() + out := make([]domain.StarGift, 0) + for rows.Next() { + gift, err := scanCatalogGift(rows) + if err != nil { + return nil, err + } + out = append(out, gift) + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate star gift catalog: %w", err) + } + return out, nil +} + +func (s *StarGiftStore) CatalogGift(ctx context.Context, giftID int64) (domain.StarGift, bool, error) { + if giftID <= 0 { + return domain.StarGift{}, false, nil + } + gift, err := scanCatalogGift(s.db.QueryRow(ctx, starGiftCatalogSelect+` +WHERE c.enabled AND c.gift_id = $1`, giftID)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.StarGift{}, false, nil + } + if err != nil { + return domain.StarGift{}, false, err + } + return gift, true, nil +} + +func (s *StarGiftStore) CatalogRevision(ctx context.Context, revisionID int64) (domain.StarGift, bool, error) { + if revisionID <= 0 { + return domain.StarGift{}, false, nil + } + gift, err := scanCatalogGift(s.db.QueryRow(ctx, ` +SELECT r.gift_id, r.id, r.stars, r.convert_stars, r.title, + COALESCE(cr.upgrade_stars, 0), COALESCE(cr.supply_total, 0), COALESCE(cr.issued, 0), + d.id, d.access_hash, d.file_reference, d.date, d.mime_type, d.size, d.dc_id, + d.attributes::text, d.thumbs::text +FROM star_gift_catalog_revisions r +JOIN star_gift_catalog c ON c.gift_id = r.gift_id +LEFT JOIN star_gift_collectible_revisions cr ON cr.id = c.collectible_revision_id AND cr.status = 'published' +JOIN documents d ON d.id = r.document_id +WHERE r.id = $1`, revisionID)) + if errors.Is(err, pgx.ErrNoRows) { + return domain.StarGift{}, false, nil + } + if err != nil { + return domain.StarGift{}, false, err + } + return gift, true, nil +} + +func scanCatalogGift(row rowScanner) (domain.StarGift, error) { + var gift domain.StarGift + var attrsJSON, thumbsJSON string + if err := row.Scan( + &gift.ID, &gift.RevisionID, &gift.Stars, &gift.ConvertStars, &gift.Title, + &gift.UpgradeStars, &gift.UpgradeTotal, &gift.UpgradeIssued, + &gift.Sticker.ID, &gift.Sticker.AccessHash, &gift.Sticker.FileReference, &gift.Sticker.Date, + &gift.Sticker.MimeType, &gift.Sticker.Size, &gift.Sticker.DCID, &attrsJSON, &thumbsJSON, + ); err != nil { + return domain.StarGift{}, err + } + attrs, err := decodeDocumentAttributes(attrsJSON) + if err != nil { + return domain.StarGift{}, fmt.Errorf("decode star gift document attributes: %w", err) + } + thumbs, err := decodePhotoSizes(thumbsJSON) + if err != nil { + return domain.StarGift{}, fmt.Errorf("decode star gift document thumbs: %w", err) + } + gift.Sticker.Attributes = attrs + gift.Sticker.Thumbs = thumbs + if !gift.Sticker.IsSticker() || gift.Sticker.MimeType != "application/x-tgsticker" { + return domain.StarGift{}, fmt.Errorf("invalid star gift revision %d document %d", gift.RevisionID, gift.Sticker.ID) + } + return gift, nil +} + +func (s *StarGiftStore) CreateCatalogRevision(ctx context.Context, write domain.StarGiftCatalogWrite) (domain.StarGiftCatalogEntry, error) { + if write.Stars <= 0 || write.ConvertStars < 0 || write.ConvertStars > write.Stars || + write.Document.ID <= 0 || !write.Document.IsSticker() || write.Document.MimeType != "application/x-tgsticker" || + len(write.Animation.JSON) == 0 || len(write.Animation.SHA256) != 32 { + return domain.StarGiftCatalogEntry{}, domain.ErrStarGiftInvalid + } + var entry domain.StarGiftCatalogEntry + err := withTx(ctx, s.db, "create star gift catalog revision", func(tx pgx.Tx) error { + giftID := write.GiftID + var revisionID int64 + if err := tx.QueryRow(ctx, `SELECT nextval('star_gift_catalog_revision_id_seq')`).Scan(&revisionID); err != nil { + return fmt.Errorf("allocate star gift revision id: %w", err) + } + revision := 1 + if giftID == 0 { + if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended('star_gift_catalog', 0))`); err != nil { + return fmt.Errorf("lock star gift catalog capacity: %w", err) + } + var catalogCount int + if err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_catalog`).Scan(&catalogCount); err != nil { + return fmt.Errorf("count star gift catalog: %w", err) + } + if catalogCount >= domain.MaxStarGiftCatalogSize { + return domain.ErrStarGiftCatalogFull + } + if err := tx.QueryRow(ctx, `SELECT nextval('star_gift_catalog_gift_id_seq')`).Scan(&giftID); err != nil { + return fmt.Errorf("allocate star gift id: %w", err) + } + if _, err := tx.Exec(ctx, ` +INSERT INTO star_gift_catalog (gift_id, active_revision_id, enabled, sort_order) +VALUES ($1,$2,$3,$4)`, giftID, revisionID, write.Enabled, write.SortOrder); err != nil { + return fmt.Errorf("insert star gift catalog: %w", err) + } + } else { + var ignored int64 + if err := tx.QueryRow(ctx, ` +SELECT active_revision_id FROM star_gift_catalog WHERE gift_id=$1 FOR UPDATE`, giftID).Scan(&ignored); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return domain.ErrStarGiftNotFound + } + return fmt.Errorf("lock star gift catalog: %w", err) + } + if err := tx.QueryRow(ctx, ` +SELECT COALESCE(MAX(revision), 0) + 1 +FROM star_gift_catalog_revisions +WHERE gift_id = $1`, giftID).Scan(&revision); err != nil { + return fmt.Errorf("lock star gift catalog: %w", err) + } + } + + media := NewMediaStore(tx) + if err := media.PutDocument(ctx, write.Document); err != nil { + return fmt.Errorf("put star gift document: %w", err) + } + if err := media.PutFileBlob(ctx, write.Blob); err != nil { + return fmt.Errorf("put star gift blob: %w", err) + } + if _, err := tx.Exec(ctx, ` +INSERT INTO star_gift_catalog_revisions ( + id, gift_id, revision, title, stars, convert_stars, document_id, + animation_json, animation_sha256, source_name, source_format, + width, height, frame_rate, in_point, out_point, created_by, command_id +) VALUES ($1,$2,$3,$4,$5,$6,$7,$8::jsonb,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)`, + revisionID, giftID, revision, write.Title, write.Stars, write.ConvertStars, write.Document.ID, + string(write.Animation.JSON), write.Animation.SHA256, write.Animation.SourceName, string(write.Animation.SourceFormat), + write.Animation.Width, write.Animation.Height, write.Animation.FrameRate, write.Animation.InPoint, write.Animation.OutPoint, + write.Actor, write.CommandID, + ); err != nil { + return fmt.Errorf("insert star gift revision: %w", err) + } + if write.GiftID != 0 { + if _, err := tx.Exec(ctx, ` +UPDATE star_gift_catalog +SET active_revision_id=$2, enabled=$3, sort_order=$4, updated_at=now() +WHERE gift_id=$1`, giftID, revisionID, write.Enabled, write.SortOrder); err != nil { + return fmt.Errorf("activate star gift revision: %w", err) + } + } + write.GiftID = giftID + var err error + entry, err = catalogEntryByID(ctx, tx, giftID) + return err + }) + if err != nil { + return domain.StarGiftCatalogEntry{}, err + } + return entry, nil +} + +func (s *StarGiftStore) SetCatalogEnabled(ctx context.Context, giftID int64, enabled bool) (bool, error) { + tag, err := s.db.Exec(ctx, ` +UPDATE star_gift_catalog SET enabled=$2, updated_at=now() +WHERE gift_id=$1 AND enabled IS DISTINCT FROM $2`, giftID, enabled) + if err != nil { + return false, fmt.Errorf("set star gift enabled: %w", err) + } + if tag.RowsAffected() > 0 { + return true, nil + } + var exists bool + if err := s.db.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM star_gift_catalog WHERE gift_id=$1)`, giftID).Scan(&exists); err != nil { + return false, fmt.Errorf("check star gift enabled target: %w", err) + } + if !exists { + return false, domain.ErrStarGiftNotFound + } + return false, nil +} + +func (s *StarGiftStore) SetCatalogSortOrder(ctx context.Context, giftID int64, sortOrder int) (bool, error) { + tag, err := s.db.Exec(ctx, ` +UPDATE star_gift_catalog SET sort_order=$2, updated_at=now() +WHERE gift_id=$1 AND sort_order IS DISTINCT FROM $2`, giftID, sortOrder) + if err != nil { + return false, fmt.Errorf("set star gift sort order: %w", err) + } + if tag.RowsAffected() > 0 { + return true, nil + } + var exists bool + if err := s.db.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM star_gift_catalog WHERE gift_id=$1)`, giftID).Scan(&exists); err != nil { + return false, fmt.Errorf("check star gift sort target: %w", err) + } + if !exists { + return false, domain.ErrStarGiftNotFound + } + return false, nil +} + +func (s *StarGiftStore) AnimationJSON(ctx context.Context, giftID int64) ([]byte, bool, error) { + var raw []byte + err := s.db.QueryRow(ctx, ` +SELECT r.animation_json::text +FROM star_gift_catalog c +JOIN star_gift_catalog_revisions r ON r.id = c.active_revision_id +WHERE c.gift_id=$1`, giftID).Scan(&raw) + if errors.Is(err, pgx.ErrNoRows) { + return nil, false, nil + } + if err != nil { + return nil, false, fmt.Errorf("get star gift animation: %w", err) + } + return raw, true, nil +} + +func catalogEntryByID(ctx context.Context, db sqlcgen.DBTX, giftID int64) (domain.StarGiftCatalogEntry, error) { + row := db.QueryRow(ctx, ` +SELECT c.gift_id, r.id, r.stars, r.convert_stars, r.title, + COALESCE(cr.upgrade_stars, 0), COALESCE(cr.supply_total, 0), COALESCE(cr.issued, 0), + d.id, d.access_hash, d.file_reference, d.date, d.mime_type, d.size, d.dc_id, + d.attributes::text, d.thumbs::text, + c.enabled, c.sort_order, r.revision, r.source_name, r.source_format, + r.animation_sha256, r.width, r.height, r.frame_rate, r.created_by, c.updated_at, + (SELECT COUNT(*) FROM peer_star_gifts p WHERE p.gift_id=c.gift_id) +FROM star_gift_catalog c +JOIN star_gift_catalog_revisions r ON r.id=c.active_revision_id +LEFT JOIN star_gift_collectible_revisions cr ON cr.id=c.collectible_revision_id AND cr.status='published' +JOIN documents d ON d.id=r.document_id +WHERE c.gift_id=$1`, giftID) + var entry domain.StarGiftCatalogEntry + var attrsJSON, thumbsJSON, sourceFormat string + if err := row.Scan( + &entry.Gift.ID, &entry.Gift.RevisionID, &entry.Gift.Stars, &entry.Gift.ConvertStars, &entry.Gift.Title, + &entry.Gift.UpgradeStars, &entry.Gift.UpgradeTotal, &entry.Gift.UpgradeIssued, + &entry.Gift.Sticker.ID, &entry.Gift.Sticker.AccessHash, &entry.Gift.Sticker.FileReference, &entry.Gift.Sticker.Date, + &entry.Gift.Sticker.MimeType, &entry.Gift.Sticker.Size, &entry.Gift.Sticker.DCID, &attrsJSON, &thumbsJSON, + &entry.Enabled, &entry.SortOrder, &entry.Revision, &entry.SourceName, &sourceFormat, + &entry.AnimationSHA, &entry.Width, &entry.Height, &entry.FrameRate, &entry.CreatedBy, &entry.UpdatedAt, + &entry.ReceivedCount, + ); err != nil { + return domain.StarGiftCatalogEntry{}, err + } + attrs, err := decodeDocumentAttributes(attrsJSON) + if err != nil { + return domain.StarGiftCatalogEntry{}, err + } + thumbs, err := decodePhotoSizes(thumbsJSON) + if err != nil { + return domain.StarGiftCatalogEntry{}, err + } + entry.Gift.Sticker.Attributes = attrs + entry.Gift.Sticker.Thumbs = thumbs + entry.SourceFormat = domain.StarGiftAnimationFormat(sourceFormat) + entry.AnimationSize = entry.Gift.Sticker.Size + return entry, nil +} + func (s *StarGiftStore) Create(ctx context.Context, gift domain.SavedStarGift) (int64, error) { if !validSavedStarGift(gift) { return 0, domain.ErrStarGiftInvalid @@ -30,14 +315,14 @@ func (s *StarGiftStore) Create(ctx context.Context, gift domain.SavedStarGift) ( WITH next_id AS ( SELECT nextval(pg_get_serial_sequence('public.peer_star_gifts', 'id'))::bigint AS id ) -INSERT INTO peer_star_gifts (id, owner_peer_type, owner_peer_id, from_user_id, gift_id, msg_id, saved_id, gift_date, name_hidden, unsaved, converted, convert_stars, message) -SELECT next_id.id, $1,$2,$3,$4,$5, - CASE WHEN $1 = 'channel' AND $6::bigint = 0 THEN next_id.id ELSE $6::bigint END, - $7,$8,$9,false,$10,$11 +INSERT INTO peer_star_gifts (id, owner_peer_type, owner_peer_id, from_user_id, gift_id, catalog_revision_id, msg_id, saved_id, gift_date, name_hidden, unsaved, converted, convert_stars, prepaid_upgrade_stars, message) +SELECT next_id.id, $1,$2,$3,$4,$5,$6, + CASE WHEN $1 = 'channel' AND $7::bigint = 0 THEN next_id.id ELSE $7::bigint END, + $8,$9,$10,false,$11,$12,$13 FROM next_id RETURNING id`, - string(gift.Owner.Type), gift.Owner.ID, gift.FromUserID, gift.GiftID, gift.MsgID, gift.SavedID, gift.Date, - gift.NameHidden, gift.Unsaved, gift.ConvertStars, gift.Message).Scan(&id) + string(gift.Owner.Type), gift.Owner.ID, gift.FromUserID, gift.GiftID, gift.RevisionID, gift.MsgID, gift.SavedID, gift.Date, + gift.NameHidden, gift.Unsaved, gift.ConvertStars, gift.PrepaidUpgradeStars, gift.Message).Scan(&id) if err != nil { return 0, fmt.Errorf("create star gift: %w", err) } @@ -45,38 +330,80 @@ RETURNING id`, } func (s *StarGiftStore) ListByOwner(ctx context.Context, owner domain.Peer, excludeUnsaved bool, offset string, limit int) (domain.SavedStarGiftPage, error) { + return s.ListByOwnerFiltered(ctx, domain.SavedStarGiftFilter{ + Owner: owner, ExcludeUnsaved: excludeUnsaved, Offset: offset, Limit: limit, + }) +} + +func (s *StarGiftStore) ListByOwnerFiltered(ctx context.Context, filter domain.SavedStarGiftFilter) (domain.SavedStarGiftPage, error) { + owner, offset, limit := filter.Owner, filter.Offset, filter.Limit if !validStarGiftOwner(owner) { return domain.SavedStarGiftPage{}, nil } if limit <= 0 || limit > domain.MaxSavedStarGiftsLimit { limit = domain.MaxSavedStarGiftsLimit } - // 总数(未转换 + 可选 excludeUnsaved 过滤)。 - countQuery := `SELECT COUNT(*) FROM peer_star_gifts WHERE owner_peer_type = $1 AND owner_peer_id = $2 AND NOT converted` - if excludeUnsaved { - countQuery += ` AND NOT unsaved` + joins := ` +JOIN star_gift_catalog c ON c.gift_id = p.gift_id +LEFT JOIN star_gift_collectible_revisions acr + ON acr.id = c.collectible_revision_id AND acr.status = 'published'` + conditions := []string{"p.owner_peer_type = $1", "p.owner_peer_id = $2", "NOT p.converted"} + args := []any{string(owner.Type), owner.ID} + if filter.ExcludeUnsaved { + conditions = append(conditions, "NOT p.unsaved") } + if filter.ExcludeSaved { + conditions = append(conditions, "p.unsaved") + } + if filter.ExcludeUnique { + conditions = append(conditions, "p.unique_gift_id IS NULL") + } + // telesrv ordinary catalog gifts are currently unlimited. Unique gifts are + // collectibles and therefore survive exclude_unlimited. + if filter.ExcludeUnlimited { + conditions = append(conditions, "p.unique_gift_id IS NOT NULL") + } + upgradable := `(p.unique_gift_id IS NULL AND acr.id IS NOT NULL AND acr.upgrade_stars > 0 AND acr.issued < acr.supply_total)` + if filter.ExcludeUpgradable { + conditions = append(conditions, "NOT "+upgradable) + } + if filter.ExcludeUnupgradable { + conditions = append(conditions, upgradable) + } + if filter.CollectionID > 0 { + args = append(args, filter.CollectionID) + conditions = append(conditions, fmt.Sprintf(`EXISTS ( +SELECT 1 FROM star_gift_collection_items ci +JOIN star_gift_collections cc ON cc.collection_id = ci.collection_id +WHERE ci.saved_gift_id = p.id AND ci.collection_id = $%d + AND cc.owner_peer_type = p.owner_peer_type AND cc.owner_peer_id = p.owner_peer_id)`, len(args))) + } + where := strings.Join(conditions, " AND ") + countQuery := `SELECT COUNT(*) FROM peer_star_gifts p ` + joins + ` WHERE ` + where var total int - if err := s.db.QueryRow(ctx, countQuery, string(owner.Type), owner.ID).Scan(&total); err != nil { + if err := s.db.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil { return domain.SavedStarGiftPage{}, fmt.Errorf("count star gifts: %w", err) } page := domain.SavedStarGiftPage{Count: total} - where := "owner_peer_type = $1 AND owner_peer_id = $2 AND NOT converted" - if excludeUnsaved { - where += " AND NOT unsaved" - } - args := []any{string(owner.Type), owner.ID, limit + 1} if cursor, ok := domain.DecodeStarGiftCursor(offset); ok { - where += " AND id < $4" args = append(args, cursor) + where += fmt.Sprintf(" AND p.id < $%d", len(args)) } + args = append(args, limit+1) + limitPlaceholder := len(args) rows, err := s.db.Query(ctx, ` -SELECT id, owner_peer_type, owner_peer_id, from_user_id, gift_id, msg_id, saved_id, gift_date, name_hidden, unsaved, converted, convert_stars, message -FROM peer_star_gifts +SELECT p.id, p.owner_peer_type, p.owner_peer_id, p.from_user_id, p.gift_id, p.catalog_revision_id, + p.msg_id, p.saved_id, p.gift_date, p.name_hidden, p.unsaved, p.converted, p.convert_stars, p.prepaid_upgrade_stars, + p.message, COALESCE(p.unique_gift_id, 0), p.upgrade_msg_id, p.pinned_order, + COALESCE((SELECT array_agg(i.collection_id ORDER BY c.sort_order, i.collection_id) + FROM star_gift_collection_items i + JOIN star_gift_collections c ON c.collection_id=i.collection_id + WHERE i.saved_gift_id=p.id), ARRAY[]::integer[]) +FROM peer_star_gifts p `+joins+` WHERE `+where+` -ORDER BY id DESC -LIMIT $3`, args...) +ORDER BY p.id DESC +LIMIT $`+fmt.Sprint(limitPlaceholder), args...) if err != nil { return domain.SavedStarGiftPage{}, fmt.Errorf("list star gifts: %w", err) } @@ -100,14 +427,73 @@ LIMIT $3`, args...) return page, nil } +func (s *StarGiftStore) ResolveSavedIDs(ctx context.Context, owner domain.Peer, refs []domain.SavedStarGiftRef) ([]int64, error) { + if !validStarGiftOwner(owner) || len(refs) > domain.MaxStarGiftCollectionItems { + return nil, domain.ErrStarGiftCollectibleInvalid + } + if len(refs) == 0 { + return []int64{}, nil + } + values := make([]int64, 0, len(refs)) + seenValues := make(map[int64]struct{}, len(refs)) + column := "msg_id" + for _, ref := range refs { + if ref.Owner != owner || !ref.Valid() { + return nil, domain.ErrStarGiftNotFound + } + value := int64(ref.MsgID) + if owner.Type == domain.PeerTypeChannel { + column = "saved_id" + value = ref.SavedID + } + if _, duplicate := seenValues[value]; duplicate { + return nil, domain.ErrStarGiftCollectibleInvalid + } + seenValues[value] = struct{}{} + values = append(values, value) + } + rows, err := s.db.Query(ctx, `SELECT `+column+`::bigint, id FROM peer_star_gifts +WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND NOT converted AND `+column+`::bigint=ANY($3::bigint[])`, string(owner.Type), owner.ID, values) + if err != nil { + return nil, fmt.Errorf("resolve saved star gifts: %w", err) + } + defer rows.Close() + resolved := make(map[int64]int64, len(values)) + for rows.Next() { + var value, id int64 + if err := rows.Scan(&value, &id); err != nil { + return nil, fmt.Errorf("scan resolved saved star gift: %w", err) + } + resolved[value] = id + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate resolved saved star gifts: %w", err) + } + out := make([]int64, 0, len(values)) + for _, value := range values { + id := resolved[value] + if id == 0 { + return nil, domain.ErrStarGiftNotFound + } + out = append(out, id) + } + return out, nil +} + func (s *StarGiftStore) GetByRef(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, bool, error) { if !ref.Valid() { return domain.SavedStarGift{}, false, nil } where, args := savedStarGiftRefWhere(ref) row := s.db.QueryRow(ctx, ` -SELECT id, owner_peer_type, owner_peer_id, from_user_id, gift_id, msg_id, saved_id, gift_date, name_hidden, unsaved, converted, convert_stars, message -FROM peer_star_gifts +SELECT p.id, p.owner_peer_type, p.owner_peer_id, p.from_user_id, p.gift_id, p.catalog_revision_id, + p.msg_id, p.saved_id, p.gift_date, p.name_hidden, p.unsaved, p.converted, p.convert_stars, p.prepaid_upgrade_stars, + p.message, COALESCE(p.unique_gift_id, 0), p.upgrade_msg_id, p.pinned_order, + COALESCE((SELECT array_agg(i.collection_id ORDER BY c.sort_order, i.collection_id) + FROM star_gift_collection_items i + JOIN star_gift_collections c ON c.collection_id=i.collection_id + WHERE i.saved_gift_id=p.id), ARRAY[]::integer[]) +FROM peer_star_gifts p WHERE `+where, args...) g, err := scanSavedStarGift(row) if err != nil { @@ -151,10 +537,19 @@ func (s *StarGiftStore) MarkConverted(ctx context.Context, ref domain.SavedStarG } out := domain.SavedStarGift{} err := withTx(ctx, s.db, "convert star gift", func(tx pgx.Tx) error { + if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(ref.Owner)); err != nil { + return fmt.Errorf("lock star gift owner collections: %w", err) + } where, args := savedStarGiftRefWhere(ref) row := tx.QueryRow(ctx, ` -SELECT id, owner_peer_type, owner_peer_id, from_user_id, gift_id, msg_id, saved_id, gift_date, name_hidden, unsaved, converted, convert_stars, message -FROM peer_star_gifts +SELECT p.id, p.owner_peer_type, p.owner_peer_id, p.from_user_id, p.gift_id, p.catalog_revision_id, + p.msg_id, p.saved_id, p.gift_date, p.name_hidden, p.unsaved, p.converted, p.convert_stars, p.prepaid_upgrade_stars, + p.message, COALESCE(p.unique_gift_id, 0), p.upgrade_msg_id, p.pinned_order, + COALESCE((SELECT array_agg(i.collection_id ORDER BY c.sort_order, i.collection_id) + FROM star_gift_collection_items i + JOIN star_gift_collections c ON c.collection_id=i.collection_id + WHERE i.saved_gift_id=p.id), ARRAY[]::integer[]) +FROM peer_star_gifts p WHERE `+where+` FOR UPDATE`, args...) g, err := scanSavedStarGift(row) if err != nil { @@ -166,11 +561,19 @@ WHERE `+where+` FOR UPDATE`, args...) if g.Converted { return domain.ErrStarGiftAlreadyConverted } - if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET converted = true, unsaved = true WHERE id = $1`, g.ID); err != nil { + if g.UniqueGiftID != 0 { + return domain.ErrStarGiftAlreadyUpgraded + } + if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET converted = true, unsaved = true, pinned_order = 0 WHERE id = $1`, g.ID); err != nil { return fmt.Errorf("mark star gift converted: %w", err) } + if err := removeSavedGiftFromCollections(ctx, tx, g.Owner, g.ID); err != nil { + return err + } g.Converted = true g.Unsaved = true + g.PinnedOrder = 0 + g.CollectionIDs = nil out = g return nil }) @@ -183,8 +586,9 @@ WHERE `+where+` FOR UPDATE`, args...) func scanSavedStarGift(row rowScanner) (domain.SavedStarGift, error) { var g domain.SavedStarGift var ownerType string - if err := row.Scan(&g.ID, &ownerType, &g.Owner.ID, &g.FromUserID, &g.GiftID, &g.MsgID, &g.SavedID, &g.Date, - &g.NameHidden, &g.Unsaved, &g.Converted, &g.ConvertStars, &g.Message); err != nil { + if err := row.Scan(&g.ID, &ownerType, &g.Owner.ID, &g.FromUserID, &g.GiftID, &g.RevisionID, &g.MsgID, &g.SavedID, &g.Date, + &g.NameHidden, &g.Unsaved, &g.Converted, &g.ConvertStars, &g.PrepaidUpgradeStars, &g.Message, &g.UniqueGiftID, + &g.UpgradeMsgID, &g.PinnedOrder, &g.CollectionIDs); err != nil { return domain.SavedStarGift{}, err } g.Owner.Type = domain.PeerType(ownerType) @@ -204,7 +608,7 @@ func savedStarGiftRefWhere(ref domain.SavedStarGiftRef) (string, []any) { } func validSavedStarGift(g domain.SavedStarGift) bool { - if g.GiftID == 0 || !validStarGiftOwner(g.Owner) { + if g.GiftID == 0 || g.RevisionID == 0 || !validStarGiftOwner(g.Owner) { return false } switch g.Owner.Type { diff --git a/internal/store/postgres/star_gift_collectibles.go b/internal/store/postgres/star_gift_collectibles.go new file mode 100644 index 00000000..2d2a2be6 --- /dev/null +++ b/internal/store/postgres/star_gift_collectibles.go @@ -0,0 +1,781 @@ +package postgres + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + + "telesrv/internal/domain" + "telesrv/internal/store/postgres/sqlcgen" +) + +func (s *StarGiftStore) PublishCollectibleRevision(ctx context.Context, write domain.StarGiftCollectibleWrite) (domain.StarGiftCollectibleRevision, error) { + write.SlugPrefix = strings.ToLower(strings.TrimSpace(write.SlugPrefix)) + write.Actor = strings.TrimSpace(write.Actor) + write.CommandID = strings.TrimSpace(write.CommandID) + if err := domain.ValidateStarGiftCollectibleWrite(write); err != nil { + return domain.StarGiftCollectibleRevision{}, err + } + var result domain.StarGiftCollectibleRevision + err := withTx(ctx, s.db, "publish collectible star gift revision", func(tx pgx.Tx) error { + var ignored int64 + if err := tx.QueryRow(ctx, `SELECT gift_id FROM star_gift_catalog WHERE gift_id=$1 FOR UPDATE`, write.GiftID).Scan(&ignored); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return domain.ErrStarGiftNotFound + } + return fmt.Errorf("lock collectible catalog gift: %w", err) + } + var revision int + if err := tx.QueryRow(ctx, ` +SELECT COALESCE(MAX(revision), 0) + 1 FROM star_gift_collectible_revisions WHERE gift_id=$1`, write.GiftID).Scan(&revision); err != nil { + return fmt.Errorf("allocate collectible revision: %w", err) + } + var revisionID int64 + if err := tx.QueryRow(ctx, ` +INSERT INTO star_gift_collectible_revisions + (gift_id, revision, upgrade_stars, supply_total, slug_prefix, status, created_by, command_id) +VALUES ($1,$2,$3,$4,$5,'draft',$6,$7) +RETURNING id`, write.GiftID, revision, write.UpgradeStars, write.SupplyTotal, write.SlugPrefix, write.Actor, write.CommandID).Scan(&revisionID); err != nil { + return fmt.Errorf("insert collectible revision: %w", err) + } + media := NewMediaStore(tx) + insertAnimated := func(table string, attributes []domain.StarGiftCollectibleAttribute) error { + for _, attribute := range attributes { + if err := media.PutDocument(ctx, *attribute.Document); err != nil { + return fmt.Errorf("put collectible %s document: %w", attribute.Kind, err) + } + if err := media.PutFileBlob(ctx, *attribute.Blob); err != nil { + return fmt.Errorf("put collectible %s blob: %w", attribute.Kind, err) + } + animation := attribute.Animation + query := fmt.Sprintf(` +INSERT INTO %s + (collectible_revision_id, name, document_id, animation_json, animation_sha256, + source_name, source_format, width, height, frame_rate, in_point, out_point, + rarity_permille, sort_order) +VALUES ($1,$2,$3,$4::jsonb,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`, table) + if _, err := tx.Exec(ctx, query, revisionID, strings.TrimSpace(attribute.Name), attribute.Document.ID, + string(animation.JSON), animation.SHA256, animation.SourceName, string(animation.SourceFormat), + animation.Width, animation.Height, animation.FrameRate, animation.InPoint, animation.OutPoint, + attribute.RarityPermille, attribute.SortOrder); err != nil { + return fmt.Errorf("insert collectible %s attribute: %w", attribute.Kind, err) + } + } + return nil + } + if err := insertAnimated("star_gift_collectible_models", write.Models); err != nil { + return err + } + if err := insertAnimated("star_gift_collectible_patterns", write.Patterns); err != nil { + return err + } + for _, attribute := range write.Backdrops { + if _, err := tx.Exec(ctx, ` +INSERT INTO star_gift_collectible_backdrops + (collectible_revision_id, name, backdrop_id, center_color, edge_color, pattern_color, + text_color, rarity_permille, sort_order) +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`, revisionID, strings.TrimSpace(attribute.Name), attribute.BackdropID, + attribute.CenterColor, attribute.EdgeColor, attribute.PatternColor, attribute.TextColor, + attribute.RarityPermille, attribute.SortOrder); err != nil { + return fmt.Errorf("insert collectible backdrop: %w", err) + } + } + if _, err := tx.Exec(ctx, ` +UPDATE star_gift_collectible_revisions SET status='published', published_at=now() WHERE id=$1`, revisionID); err != nil { + return fmt.Errorf("publish collectible revision: %w", err) + } + if _, err := tx.Exec(ctx, ` +UPDATE star_gift_catalog SET collectible_revision_id=$2, updated_at=now() WHERE gift_id=$1`, write.GiftID, revisionID); err != nil { + return fmt.Errorf("activate collectible revision: %w", err) + } + var err error + result, err = collectibleRevisionByID(ctx, tx, revisionID) + return err + }) + return result, err +} + +func (s *StarGiftStore) ActiveCollectibleRevision(ctx context.Context, giftID int64) (domain.StarGiftCollectibleRevision, bool, error) { + var revisionID int64 + err := s.db.QueryRow(ctx, ` +SELECT collectible_revision_id FROM star_gift_catalog +WHERE gift_id=$1 AND collectible_revision_id IS NOT NULL`, giftID).Scan(&revisionID) + if errors.Is(err, pgx.ErrNoRows) { + return domain.StarGiftCollectibleRevision{}, false, nil + } + if err != nil { + return domain.StarGiftCollectibleRevision{}, false, fmt.Errorf("get active collectible revision: %w", err) + } + revision, err := collectibleRevisionByID(ctx, s.db, revisionID) + if err != nil { + return domain.StarGiftCollectibleRevision{}, false, err + } + return revision, true, nil +} + +func (s *StarGiftStore) CollectibleAvailability(ctx context.Context, giftIDs []int64) (map[int64]domain.StarGiftCollectibleAvailability, error) { + out := make(map[int64]domain.StarGiftCollectibleAvailability, len(giftIDs)) + if len(giftIDs) == 0 { + return out, nil + } + rows, err := s.db.Query(ctx, ` +SELECT c.gift_id, r.upgrade_stars, r.supply_total, r.issued +FROM star_gift_catalog c +JOIN star_gift_collectible_revisions r ON r.id=c.collectible_revision_id +WHERE c.gift_id=ANY($1) AND r.status='published'`, giftIDs) + if err != nil { + return nil, fmt.Errorf("list collectible availability: %w", err) + } + defer rows.Close() + for rows.Next() { + var giftID int64 + var availability domain.StarGiftCollectibleAvailability + if err := rows.Scan(&giftID, &availability.UpgradeStars, &availability.SupplyTotal, &availability.Issued); err != nil { + return nil, fmt.Errorf("scan collectible availability: %w", err) + } + out[giftID] = availability + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("list collectible availability rows: %w", err) + } + return out, nil +} + +func collectibleRevisionByID(ctx context.Context, db sqlcgen.DBTX, revisionID int64) (domain.StarGiftCollectibleRevision, error) { + var revision domain.StarGiftCollectibleRevision + var status string + var publishedAt pgtype.Timestamptz + if err := db.QueryRow(ctx, ` +SELECT id, gift_id, revision, upgrade_stars, supply_total, issued, slug_prefix, status, + created_by, created_at, published_at +FROM star_gift_collectible_revisions WHERE id=$1`, revisionID).Scan( + &revision.ID, &revision.GiftID, &revision.Revision, &revision.UpgradeStars, &revision.SupplyTotal, + &revision.Issued, &revision.SlugPrefix, &status, &revision.CreatedBy, &revision.CreatedAt, &publishedAt, + ); err != nil { + return domain.StarGiftCollectibleRevision{}, fmt.Errorf("get collectible revision: %w", err) + } + revision.Published = status == "published" + if publishedAt.Valid { + revision.PublishedAt = publishedAt.Time + } + var err error + if revision.Models, err = listAnimatedCollectibleAttributes(ctx, db, revisionID, domain.StarGiftCollectibleModel); err != nil { + return domain.StarGiftCollectibleRevision{}, err + } + if revision.Patterns, err = listAnimatedCollectibleAttributes(ctx, db, revisionID, domain.StarGiftCollectiblePattern); err != nil { + return domain.StarGiftCollectibleRevision{}, err + } + if revision.Backdrops, err = listCollectibleBackdrops(ctx, db, revisionID); err != nil { + return domain.StarGiftCollectibleRevision{}, err + } + return revision, nil +} + +func listAnimatedCollectibleAttributes(ctx context.Context, db sqlcgen.DBTX, revisionID int64, kind domain.StarGiftCollectibleAttributeKind) ([]domain.StarGiftCollectibleAttribute, error) { + table := "star_gift_collectible_models" + if kind == domain.StarGiftCollectiblePattern { + table = "star_gift_collectible_patterns" + } else if kind != domain.StarGiftCollectibleModel { + return nil, domain.ErrStarGiftCollectibleInvalid + } + rows, err := db.Query(ctx, fmt.Sprintf(` +SELECT a.id, a.collectible_revision_id, a.name, a.rarity_permille, a.sort_order, + a.animation_json::text, a.animation_sha256, a.source_name, a.source_format, + a.width, a.height, a.frame_rate, a.in_point, a.out_point, + d.id, d.access_hash, d.file_reference, d.date, d.mime_type, d.size, d.dc_id, + d.attributes::text, d.thumbs::text +FROM %s a JOIN documents d ON d.id=a.document_id +WHERE a.collectible_revision_id=$1 ORDER BY a.sort_order, a.id`, table), revisionID) + if err != nil { + return nil, fmt.Errorf("list collectible %s attributes: %w", kind, err) + } + defer rows.Close() + out := make([]domain.StarGiftCollectibleAttribute, 0) + for rows.Next() { + attribute := domain.StarGiftCollectibleAttribute{Kind: kind, Document: &domain.Document{}, Animation: &domain.StarGiftAnimation{}} + var attrsJSON, thumbsJSON, sourceFormat string + if err := rows.Scan(&attribute.ID, &attribute.CollectibleRevisionID, &attribute.Name, &attribute.RarityPermille, &attribute.SortOrder, + &attribute.Animation.JSON, &attribute.Animation.SHA256, &attribute.Animation.SourceName, &sourceFormat, + &attribute.Animation.Width, &attribute.Animation.Height, &attribute.Animation.FrameRate, &attribute.Animation.InPoint, &attribute.Animation.OutPoint, + &attribute.Document.ID, &attribute.Document.AccessHash, &attribute.Document.FileReference, &attribute.Document.Date, + &attribute.Document.MimeType, &attribute.Document.Size, &attribute.Document.DCID, &attrsJSON, &thumbsJSON); err != nil { + return nil, err + } + attribute.Animation.SourceFormat = domain.StarGiftAnimationFormat(sourceFormat) + if attribute.Document.Attributes, err = decodeDocumentAttributes(attrsJSON); err != nil { + return nil, err + } + if attribute.Document.Thumbs, err = decodePhotoSizes(thumbsJSON); err != nil { + return nil, err + } + out = append(out, attribute) + } + return out, rows.Err() +} + +func listCollectibleBackdrops(ctx context.Context, db sqlcgen.DBTX, revisionID int64) ([]domain.StarGiftCollectibleAttribute, error) { + rows, err := db.Query(ctx, ` +SELECT id, collectible_revision_id, name, backdrop_id, center_color, edge_color, pattern_color, + text_color, rarity_permille, sort_order +FROM star_gift_collectible_backdrops WHERE collectible_revision_id=$1 ORDER BY sort_order, id`, revisionID) + if err != nil { + return nil, fmt.Errorf("list collectible backdrops: %w", err) + } + defer rows.Close() + out := make([]domain.StarGiftCollectibleAttribute, 0) + for rows.Next() { + attribute := domain.StarGiftCollectibleAttribute{Kind: domain.StarGiftCollectibleBackdrop} + if err := rows.Scan(&attribute.ID, &attribute.CollectibleRevisionID, &attribute.Name, &attribute.BackdropID, + &attribute.CenterColor, &attribute.EdgeColor, &attribute.PatternColor, &attribute.TextColor, + &attribute.RarityPermille, &attribute.SortOrder); err != nil { + return nil, err + } + out = append(out, attribute) + } + return out, rows.Err() +} + +func (s *StarGiftStore) CollectibleAnimationJSON(ctx context.Context, giftID int64, kind domain.StarGiftCollectibleAttributeKind, attributeID int64) ([]byte, bool, error) { + table := "star_gift_collectible_models" + if kind == domain.StarGiftCollectiblePattern { + table = "star_gift_collectible_patterns" + } else if kind != domain.StarGiftCollectibleModel { + return nil, false, nil + } + var raw []byte + err := s.db.QueryRow(ctx, fmt.Sprintf(` +SELECT a.animation_json::text FROM %s a +JOIN star_gift_catalog c ON c.collectible_revision_id=a.collectible_revision_id +WHERE c.gift_id=$1 AND a.id=$2`, table), giftID, attributeID).Scan(&raw) + if errors.Is(err, pgx.ErrNoRows) { + return nil, false, nil + } + if err != nil { + return nil, false, fmt.Errorf("get collectible animation: %w", err) + } + return raw, true, nil +} + +func (s *StarGiftStore) UniqueBySlug(ctx context.Context, slug string) (domain.UniqueStarGift, bool, error) { + return s.uniqueByPredicate(ctx, "u.slug=$1", strings.ToLower(strings.TrimSpace(slug))) +} + +func (s *StarGiftStore) UniqueByID(ctx context.Context, uniqueGiftID int64) (domain.UniqueStarGift, bool, error) { + return s.uniqueByPredicate(ctx, "u.id=$1", uniqueGiftID) +} + +func (s *StarGiftStore) UniqueByIDs(ctx context.Context, uniqueGiftIDs []int64) (map[int64]domain.UniqueStarGift, error) { + out := make(map[int64]domain.UniqueStarGift, len(uniqueGiftIDs)) + if len(uniqueGiftIDs) == 0 { + return out, nil + } + rows, err := s.db.Query(ctx, uniqueStarGiftQuery("u.id=ANY($1::bigint[])"), uniqueGiftIDs) + if err != nil { + return nil, fmt.Errorf("list unique star gifts: %w", err) + } + defer rows.Close() + for rows.Next() { + unique, err := scanUniqueStarGift(rows) + if err != nil { + return nil, err + } + out[unique.ID] = unique + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate unique star gifts: %w", err) + } + return out, nil +} + +func (s *StarGiftStore) uniqueByPredicate(ctx context.Context, predicate string, value any) (domain.UniqueStarGift, bool, error) { + row := s.db.QueryRow(ctx, uniqueStarGiftQuery(predicate), value) + unique, err := scanUniqueStarGift(row) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return domain.UniqueStarGift{}, false, nil + } + return domain.UniqueStarGift{}, false, err + } + return unique, true, nil +} + +func uniqueStarGiftQuery(predicate string) string { + return fmt.Sprintf(` +SELECT u.id, u.gift_id, u.collectible_revision_id, u.source_saved_gift_id, u.title, u.slug, u.num, + u.owner_peer_type, u.owner_peer_id, u.keep_original_details, u.created_at, + r.issued, r.supply_total, sg.from_user_id, sg.owner_peer_type, sg.owner_peer_id, + sg.gift_date, sg.message, sg.name_hidden, + m.id, m.name, m.rarity_permille, md.id, md.access_hash, md.file_reference, md.date, + md.mime_type, md.size, md.dc_id, md.attributes::text, md.thumbs::text, + p.id, p.name, p.rarity_permille, pd.id, pd.access_hash, pd.file_reference, pd.date, + pd.mime_type, pd.size, pd.dc_id, pd.attributes::text, pd.thumbs::text, + b.id, b.name, b.backdrop_id, b.center_color, b.edge_color, b.pattern_color, b.text_color, b.rarity_permille +FROM unique_star_gifts u +JOIN star_gift_collectible_revisions r ON r.id=u.collectible_revision_id +JOIN star_gift_collectible_models m ON m.id=u.model_attribute_id +JOIN documents md ON md.id=m.document_id +JOIN star_gift_collectible_patterns p ON p.id=u.pattern_attribute_id +JOIN documents pd ON pd.id=p.document_id +JOIN star_gift_collectible_backdrops b ON b.id=u.backdrop_attribute_id +JOIN peer_star_gifts sg ON sg.id=u.source_saved_gift_id +WHERE %s`, predicate) +} + +func scanUniqueStarGift(row rowScanner) (domain.UniqueStarGift, error) { + var unique domain.UniqueStarGift + var ownerType, originalOwnerType string + unique.Model.Kind = domain.StarGiftCollectibleModel + unique.Pattern.Kind = domain.StarGiftCollectiblePattern + unique.Backdrop.Kind = domain.StarGiftCollectibleBackdrop + unique.Model.Document = &domain.Document{} + unique.Pattern.Document = &domain.Document{} + var modelAttrs, modelThumbs, patternAttrs, patternThumbs string + if err := row.Scan(&unique.ID, &unique.GiftID, &unique.CollectibleRevisionID, &unique.SourceSavedGiftID, + &unique.Title, &unique.Slug, &unique.Num, &ownerType, &unique.Owner.ID, &unique.KeepOriginalDetails, + &unique.CreatedAt, &unique.AvailabilityIssued, &unique.AvailabilityTotal, + &unique.OriginalFromUserID, &originalOwnerType, &unique.OriginalOwner.ID, &unique.OriginalDate, + &unique.OriginalMessage, &unique.OriginalNameHidden, + &unique.Model.ID, &unique.Model.Name, &unique.Model.RarityPermille, + &unique.Model.Document.ID, &unique.Model.Document.AccessHash, &unique.Model.Document.FileReference, + &unique.Model.Document.Date, &unique.Model.Document.MimeType, &unique.Model.Document.Size, + &unique.Model.Document.DCID, &modelAttrs, &modelThumbs, + &unique.Pattern.ID, &unique.Pattern.Name, &unique.Pattern.RarityPermille, + &unique.Pattern.Document.ID, &unique.Pattern.Document.AccessHash, &unique.Pattern.Document.FileReference, + &unique.Pattern.Document.Date, &unique.Pattern.Document.MimeType, &unique.Pattern.Document.Size, + &unique.Pattern.Document.DCID, &patternAttrs, &patternThumbs, + &unique.Backdrop.ID, &unique.Backdrop.Name, &unique.Backdrop.BackdropID, &unique.Backdrop.CenterColor, + &unique.Backdrop.EdgeColor, &unique.Backdrop.PatternColor, &unique.Backdrop.TextColor, &unique.Backdrop.RarityPermille); err != nil { + return domain.UniqueStarGift{}, fmt.Errorf("get unique star gift: %w", err) + } + unique.Owner.Type = domain.PeerType(ownerType) + unique.OriginalOwner.Type = domain.PeerType(originalOwnerType) + unique.Model.CollectibleRevisionID = unique.CollectibleRevisionID + unique.Pattern.CollectibleRevisionID = unique.CollectibleRevisionID + unique.Backdrop.CollectibleRevisionID = unique.CollectibleRevisionID + var err error + if unique.Model.Document.Attributes, err = decodeDocumentAttributes(modelAttrs); err != nil { + return domain.UniqueStarGift{}, err + } + if unique.Model.Document.Thumbs, err = decodePhotoSizes(modelThumbs); err != nil { + return domain.UniqueStarGift{}, err + } + if unique.Pattern.Document.Attributes, err = decodeDocumentAttributes(patternAttrs); err != nil { + return domain.UniqueStarGift{}, err + } + if unique.Pattern.Document.Thumbs, err = decodePhotoSizes(patternThumbs); err != nil { + return domain.UniqueStarGift{}, err + } + return unique, nil +} + +func (s *StarGiftStore) ListCollections(ctx context.Context, owner domain.Peer) ([]domain.StarGiftCollection, error) { + rows, err := s.db.Query(ctx, ` +SELECT c.collection_id, c.title, c.hash, c.sort_order, c.created_at, c.updated_at, i.saved_gift_id +FROM star_gift_collections c +LEFT JOIN star_gift_collection_items i ON i.collection_id=c.collection_id +WHERE c.owner_peer_type=$1 AND c.owner_peer_id=$2 +ORDER BY c.sort_order, c.collection_id, i.sort_order, i.saved_gift_id`, string(owner.Type), owner.ID) + if err != nil { + return nil, fmt.Errorf("list star gift collections: %w", err) + } + defer rows.Close() + out := make([]domain.StarGiftCollection, 0) + index := make(map[int]int) + for rows.Next() { + var collection domain.StarGiftCollection + var giftID pgtype.Int8 + if err := rows.Scan(&collection.CollectionID, &collection.Title, &collection.Hash, &collection.SortOrder, + &collection.CreatedAt, &collection.UpdatedAt, &giftID); err != nil { + return nil, err + } + position, ok := index[collection.CollectionID] + if !ok { + collection.Owner = owner + position = len(out) + index[collection.CollectionID] = position + out = append(out, collection) + } + if giftID.Valid { + out[position].GiftIDs = append(out[position].GiftIDs, giftID.Int64) + } + } + return out, rows.Err() +} + +func (s *StarGiftStore) CreateCollection(ctx context.Context, owner domain.Peer, title string, savedGiftIDs []int64) (domain.StarGiftCollection, error) { + title = strings.TrimSpace(title) + if !validPostgresStarGiftOwner(owner) || title == "" || len([]rune(title)) > domain.MaxStarGiftCollectionTitleRunes { + return domain.StarGiftCollection{}, domain.ErrStarGiftCollectibleInvalid + } + var result domain.StarGiftCollection + err := withTx(ctx, s.db, "create star gift collection", func(tx pgx.Tx) error { + if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(owner)); err != nil { + return err + } + var count int + if err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_collections WHERE owner_peer_type=$1 AND owner_peer_id=$2`, string(owner.Type), owner.ID).Scan(&count); err != nil { + return err + } + if count >= domain.MaxStarGiftCollectionsPerPeer { + return domain.ErrStarGiftCollectionsFull + } + ids, err := validatePostgresCollectionGiftIDs(ctx, tx, owner, savedGiftIDs) + if err != nil { + return err + } + result = domain.StarGiftCollection{Owner: owner, Title: title, GiftIDs: ids, SortOrder: count} + result.Hash = domain.StarGiftCollectionHash(title, ids) + if err := tx.QueryRow(ctx, ` +INSERT INTO star_gift_collections(owner_peer_type, owner_peer_id, title, sort_order, hash) +VALUES ($1,$2,$3,$4,$5) RETURNING collection_id, created_at, updated_at`, string(owner.Type), owner.ID, + title, count, result.Hash).Scan(&result.CollectionID, &result.CreatedAt, &result.UpdatedAt); err != nil { + return err + } + return replaceCollectionItems(ctx, tx, result.CollectionID, ids) + }) + return result, err +} + +func (s *StarGiftStore) UpdateCollection(ctx context.Context, owner domain.Peer, collectionID int, patch domain.StarGiftCollectionPatch) (domain.StarGiftCollection, error) { + var result domain.StarGiftCollection + err := withTx(ctx, s.db, "update star gift collection", func(tx pgx.Tx) error { + if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(owner)); err != nil { + return err + } + if err := tx.QueryRow(ctx, ` +SELECT title, hash, sort_order, created_at, updated_at FROM star_gift_collections +WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND collection_id=$3 FOR UPDATE`, string(owner.Type), owner.ID, collectionID).Scan( + &result.Title, &result.Hash, &result.SortOrder, &result.CreatedAt, &result.UpdatedAt); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return domain.ErrStarGiftCollectionNotFound + } + return err + } + result.Owner = owner + result.CollectionID = collectionID + rows, err := tx.Query(ctx, `SELECT saved_gift_id FROM star_gift_collection_items WHERE collection_id=$1 ORDER BY sort_order, saved_gift_id`, collectionID) + if err != nil { + return err + } + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + rows.Close() + return err + } + result.GiftIDs = append(result.GiftIDs, id) + } + rows.Close() + if patch.Title != nil { + title := strings.TrimSpace(*patch.Title) + if title == "" || len([]rune(title)) > domain.MaxStarGiftCollectionTitleRunes { + return domain.ErrStarGiftCollectibleInvalid + } + result.Title = title + } + deleted := make(map[int64]struct{}, len(patch.DeleteIDs)) + for _, id := range patch.DeleteIDs { + deleted[id] = struct{}{} + } + next := make([]int64, 0, len(result.GiftIDs)+len(patch.AddIDs)) + for _, id := range result.GiftIDs { + if _, ok := deleted[id]; !ok { + next = append(next, id) + } + } + add, err := validatePostgresCollectionGiftIDs(ctx, tx, owner, patch.AddIDs) + if err != nil { + return err + } + next = appendUniquePostgresIDs(next, add...) + if patch.Order != nil { + order, err := validatePostgresCollectionGiftIDs(ctx, tx, owner, patch.Order) + if err != nil || !samePostgresIDSet(order, next) { + return domain.ErrStarGiftCollectibleInvalid + } + next = order + } + if len(next) > domain.MaxStarGiftCollectionItems { + return domain.ErrStarGiftCollectibleInvalid + } + result.GiftIDs = next + result.Hash = domain.StarGiftCollectionHash(result.Title, result.GiftIDs) + if err := tx.QueryRow(ctx, ` +UPDATE star_gift_collections SET title=$4, hash=$5, updated_at=now() +WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND collection_id=$3 RETURNING updated_at`, + string(owner.Type), owner.ID, collectionID, result.Title, result.Hash).Scan(&result.UpdatedAt); err != nil { + return err + } + return replaceCollectionItems(ctx, tx, collectionID, result.GiftIDs) + }) + return result, err +} + +func (s *StarGiftStore) DeleteCollection(ctx context.Context, owner domain.Peer, collectionID int) (bool, error) { + var changed bool + err := withTx(ctx, s.db, "delete star gift collection", func(tx pgx.Tx) error { + if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(owner)); err != nil { + return err + } + tag, err := tx.Exec(ctx, `DELETE FROM star_gift_collections WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND collection_id=$3`, string(owner.Type), owner.ID, collectionID) + if err != nil { + return err + } + changed = tag.RowsAffected() > 0 + if changed { + _, err = tx.Exec(ctx, ` +WITH ordered AS ( + SELECT collection_id, row_number() OVER (ORDER BY sort_order, collection_id) - 1 AS next_order + FROM star_gift_collections WHERE owner_peer_type=$1 AND owner_peer_id=$2 +) +UPDATE star_gift_collections c SET sort_order=o.next_order, updated_at=now() +FROM ordered o WHERE c.collection_id=o.collection_id`, string(owner.Type), owner.ID) + } + return err + }) + return changed, err +} + +func (s *StarGiftStore) ReorderCollections(ctx context.Context, owner domain.Peer, collectionIDs []int) error { + return withTx(ctx, s.db, "reorder star gift collections", func(tx pgx.Tx) error { + if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(owner)); err != nil { + return err + } + rows, err := tx.Query(ctx, `SELECT collection_id FROM star_gift_collections WHERE owner_peer_type=$1 AND owner_peer_id=$2 FOR UPDATE`, string(owner.Type), owner.ID) + if err != nil { + return err + } + existing := make([]int, 0) + for rows.Next() { + var id int + if err := rows.Scan(&id); err != nil { + rows.Close() + return err + } + existing = append(existing, id) + } + rows.Close() + if !samePostgresIntSet(existing, collectionIDs) { + return domain.ErrStarGiftCollectibleInvalid + } + for order, id := range collectionIDs { + if _, err := tx.Exec(ctx, `UPDATE star_gift_collections SET sort_order=$4, updated_at=now() WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND collection_id=$3`, string(owner.Type), owner.ID, id, order); err != nil { + return err + } + } + return nil + }) +} + +func (s *StarGiftStore) SetPinned(ctx context.Context, owner domain.Peer, savedGiftIDs []int64) error { + return withTx(ctx, s.db, "set pinned star gifts", func(tx pgx.Tx) error { + if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, starGiftCollectionLockKey(owner)); err != nil { + return err + } + ids, err := validatePostgresCollectionGiftIDs(ctx, tx, owner, savedGiftIDs) + if err != nil { + return err + } + if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET pinned_order=0 WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND pinned_order<>0`, string(owner.Type), owner.ID); err != nil { + return err + } + for order, id := range ids { + if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET pinned_order=$2 WHERE id=$1`, id, order+1); err != nil { + return err + } + } + return nil + }) +} + +func validatePostgresCollectionGiftIDs(ctx context.Context, db sqlcgen.DBTX, owner domain.Peer, ids []int64) ([]int64, error) { + ids = dedupePostgresIDs(ids) + if len(ids) > domain.MaxStarGiftCollectionItems { + return nil, domain.ErrStarGiftCollectibleInvalid + } + if len(ids) == 0 { + return []int64{}, nil + } + rows, err := db.Query(ctx, ` +SELECT id FROM peer_star_gifts +WHERE owner_peer_type=$1 AND owner_peer_id=$2 AND NOT converted AND id=ANY($3::bigint[]) +FOR UPDATE`, string(owner.Type), owner.ID, ids) + if err != nil { + return nil, err + } + defer rows.Close() + found := make(map[int64]struct{}, len(ids)) + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return nil, err + } + found[id] = struct{}{} + } + if len(found) != len(ids) { + return nil, domain.ErrStarGiftNotFound + } + return ids, rows.Err() +} + +// removeSavedGiftFromCollections runs under the owner advisory lock. It removes +// terminal gifts and updates every affected collection hash in bounded batches, +// so getStarGiftCollections cannot return NotModified for changed membership. +func removeSavedGiftFromCollections(ctx context.Context, tx pgx.Tx, owner domain.Peer, savedGiftID int64) error { + rows, err := tx.Query(ctx, ` +SELECT c.collection_id, c.title +FROM star_gift_collections c +JOIN star_gift_collection_items i ON i.collection_id=c.collection_id +WHERE c.owner_peer_type=$1 AND c.owner_peer_id=$2 AND i.saved_gift_id=$3 +ORDER BY c.collection_id +FOR UPDATE OF c`, string(owner.Type), owner.ID, savedGiftID) + if err != nil { + return fmt.Errorf("lock converted gift collections: %w", err) + } + titles := make(map[int]string) + ids := make([]int, 0) + for rows.Next() { + var id int + var title string + if err := rows.Scan(&id, &title); err != nil { + rows.Close() + return err + } + ids = append(ids, id) + titles[id] = title + } + if err := rows.Err(); err != nil { + rows.Close() + return err + } + rows.Close() + if len(ids) == 0 { + return nil + } + if _, err := tx.Exec(ctx, `DELETE FROM star_gift_collection_items WHERE saved_gift_id=$1`, savedGiftID); err != nil { + return fmt.Errorf("remove converted gift collection memberships: %w", err) + } + + memberships := make(map[int][]int64, len(ids)) + itemRows, err := tx.Query(ctx, ` +SELECT collection_id, saved_gift_id +FROM star_gift_collection_items +WHERE collection_id=ANY($1::integer[]) +ORDER BY collection_id, sort_order, saved_gift_id`, ids) + if err != nil { + return fmt.Errorf("list remaining collection memberships: %w", err) + } + for itemRows.Next() { + var collectionID int + var giftID int64 + if err := itemRows.Scan(&collectionID, &giftID); err != nil { + itemRows.Close() + return err + } + memberships[collectionID] = append(memberships[collectionID], giftID) + } + if err := itemRows.Err(); err != nil { + itemRows.Close() + return err + } + itemRows.Close() + + hashes := make([]int64, len(ids)) + for i, collectionID := range ids { + hashes[i] = domain.StarGiftCollectionHash(titles[collectionID], memberships[collectionID]) + } + if _, err := tx.Exec(ctx, ` +UPDATE star_gift_collections c SET hash=x.hash, updated_at=now() +FROM unnest($1::integer[], $2::bigint[]) AS x(collection_id, hash) +WHERE c.collection_id=x.collection_id`, ids, hashes); err != nil { + return fmt.Errorf("refresh converted gift collection hashes: %w", err) + } + return nil +} + +func replaceCollectionItems(ctx context.Context, tx pgx.Tx, collectionID int, ids []int64) error { + if _, err := tx.Exec(ctx, `DELETE FROM star_gift_collection_items WHERE collection_id=$1`, collectionID); err != nil { + return err + } + for order, id := range ids { + if _, err := tx.Exec(ctx, `INSERT INTO star_gift_collection_items(collection_id, saved_gift_id, sort_order) VALUES ($1,$2,$3)`, collectionID, id, order); err != nil { + return err + } + } + return nil +} + +func validPostgresStarGiftOwner(owner domain.Peer) bool { + return owner.ID > 0 && (owner.Type == domain.PeerTypeUser || owner.Type == domain.PeerTypeChannel) +} + +func starGiftCollectionLockKey(owner domain.Peer) string { + return fmt.Sprintf("star_gift_collection:%s:%d", owner.Type, owner.ID) +} + +func dedupePostgresIDs(ids []int64) []int64 { + out := make([]int64, 0, len(ids)) + seen := make(map[int64]struct{}, len(ids)) + for _, id := range ids { + if id <= 0 { + continue + } + if _, ok := seen[id]; !ok { + seen[id] = struct{}{} + out = append(out, id) + } + } + return out +} + +func appendUniquePostgresIDs(dst []int64, values ...int64) []int64 { + seen := make(map[int64]struct{}, len(dst)+len(values)) + for _, id := range dst { + seen[id] = struct{}{} + } + for _, id := range values { + if _, ok := seen[id]; !ok { + seen[id] = struct{}{} + dst = append(dst, id) + } + } + return dst +} + +func samePostgresIDSet(a, b []int64) bool { + if len(a) != len(b) { + return false + } + a = append([]int64(nil), a...) + b = append([]int64(nil), b...) + sort.Slice(a, func(i, j int) bool { return a[i] < a[j] }) + sort.Slice(b, func(i, j int) bool { return b[i] < b[j] }) + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func samePostgresIntSet(a, b []int) bool { + if len(a) != len(b) { + return false + } + seen := make(map[int]struct{}, len(a)) + for _, id := range a { + seen[id] = struct{}{} + } + for _, id := range b { + if _, ok := seen[id]; !ok { + return false + } + delete(seen, id) + } + return len(seen) == 0 +} diff --git a/internal/store/postgres/star_gift_collectibles_integration_test.go b/internal/store/postgres/star_gift_collectibles_integration_test.go new file mode 100644 index 00000000..72b242b9 --- /dev/null +++ b/internal/store/postgres/star_gift_collectibles_integration_test.go @@ -0,0 +1,429 @@ +package postgres + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "telesrv/internal/domain" +) + +func TestStarGiftCollectibleUpgradeAggregatePostgres(t *testing.T) { + pool := testPool(t) + ctx := context.Background() + suffix := randomSuffix(t) + users := NewUserStore(pool) + sender := createTestUser(t, ctx, users, "+1778"+suffix+"41", "CollectibleSender", "") + owner := createTestUser(t, ctx, users, "+1778"+suffix+"42", "CollectibleOwner", "") + ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID} + + gifts := NewStarGiftStore(pool) + baseDocumentID := time.Now().UnixNano() & 0x7ffffffffffff000 + entry, err := gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{ + Title: "Comet", Stars: 50, ConvertStars: 25, Enabled: true, + Document: collectibleTestDocument(baseDocumentID, "gift.tgs"), + Blob: collectibleTestBlob(baseDocumentID, "gift"), + Animation: collectibleTestAnimation("gift.tgs"), + Actor: "integration", CommandID: "catalog-" + suffix, + }) + if err != nil { + t.Fatalf("create collectible catalog gift: %v", err) + } + poolRevision, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{ + GiftID: entry.Gift.ID, UpgradeStars: 100, SupplyTotal: 10, SlugPrefix: "comet-" + suffix, + Models: []domain.StarGiftCollectibleAttribute{{ + Kind: domain.StarGiftCollectibleModel, Name: "Aurora", RarityPermille: 1000, + Document: collectibleTestDocumentPtr(baseDocumentID+1, "model.tgs"), + Blob: collectibleTestBlobPtr(baseDocumentID+1, "model"), Animation: collectibleTestAnimationPtr("model.tgs"), + }}, + Patterns: []domain.StarGiftCollectibleAttribute{{ + Kind: domain.StarGiftCollectiblePattern, Name: "Orbit", RarityPermille: 1000, + Document: collectibleTestDocumentPtr(baseDocumentID+2, "pattern.tgs"), + Blob: collectibleTestBlobPtr(baseDocumentID+2, "pattern"), Animation: collectibleTestAnimationPtr("pattern.tgs"), + }}, + Backdrops: []domain.StarGiftCollectibleAttribute{{ + Kind: domain.StarGiftCollectibleBackdrop, Name: "Midnight", BackdropID: 1, + CenterColor: 0x112233, EdgeColor: 0x223344, PatternColor: 0x334455, TextColor: 0xffffff, + RarityPermille: 1000, + }}, + Actor: "integration", CommandID: "collectibles-" + suffix, + }) + if err != nil { + t.Fatalf("publish collectible pool: %v", err) + } + if !poolRevision.Published || poolRevision.Issued != 0 || len(poolRevision.Models) != 1 || len(poolRevision.Patterns) != 1 || len(poolRevision.Backdrops) != 1 { + t.Fatalf("published pool = %+v", poolRevision) + } + availability, err := gifts.CollectibleAvailability(ctx, []int64{entry.Gift.ID, entry.Gift.ID + 1}) + if err != nil { + t.Fatalf("collectible availability: %v", err) + } + if got, ok := availability[entry.Gift.ID]; !ok || got.UpgradeStars != 100 || got.SupplyTotal != 10 || got.Issued != 0 { + t.Fatalf("collectible availability = %+v, want active published pool", availability) + } + if _, ok := availability[entry.Gift.ID+1]; ok { + t.Fatalf("unknown gift must not have collectible availability: %+v", availability) + } + if _, err := pool.Exec(ctx, `UPDATE star_gift_collectible_revisions SET issued=issued WHERE id=$1`, poolRevision.ID); err == nil { + t.Fatal("published collectible revision accepted a non-advancing issuance update") + } + var guardedIssued int + if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, poolRevision.ID).Scan(&guardedIssued); err != nil || guardedIssued != 0 { + t.Fatalf("issued after rejected manual update = %d err %v, want 0", guardedIssued, err) + } + + savedID, err := gifts.Create(ctx, domain.SavedStarGift{ + Owner: ownerPeer, FromUserID: sender.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID, + MsgID: 700001, Date: 1700001000, ConvertStars: 25, Message: "original", + }) + if err != nil { + t.Fatalf("create saved gift: %v", err) + } + stars := NewStarsStore(pool) + if _, _, err := stars.EnsureGrant(ctx, owner.ID, 1000, 1700001001); err != nil { + t.Fatalf("grant upgrade stars: %v", err) + } + messages := NewMessageStore(pool) + upgrades := NewStarGiftUpgradeStore(pool, messages) + req := domain.StarGiftUpgradeRequest{ + UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 700001}, + KeepOriginalDetails: true, ChargeStars: 100, FormID: 991, + CommandKey: "paid-" + suffix, Date: 1700001002, + } + upgraded, err := upgrades.UpgradeStarGift(ctx, req) + if err != nil { + t.Fatalf("upgrade star gift: %v", err) + } + if upgraded.Duplicate || upgraded.Unique.Num != 1 || upgraded.Unique.Slug != "comet-"+suffix+"-1" || + upgraded.Unique.Model.Name != "Aurora" || upgraded.Unique.Pattern.Name != "Orbit" || + upgraded.Unique.Backdrop.Name != "Midnight" || upgraded.Balance.Balance != 900 || + upgraded.Saved.ID != savedID || upgraded.Saved.UniqueGiftID != upgraded.Unique.ID || upgraded.Saved.UpgradeMsgID <= 0 { + t.Fatalf("upgrade result = %+v", upgraded) + } + ownerMessage := upgraded.Send.RecipientMessage + if ownerMessage.OwnerUserID != owner.ID || ownerMessage.Pts <= 0 || ownerMessage.Media == nil || + ownerMessage.Media.ServiceAction == nil || ownerMessage.Media.ServiceAction.Kind != domain.MessageServiceActionStarGiftUnique || + ownerMessage.Media.ServiceAction.StarGiftUnique == nil || ownerMessage.Media.ServiceAction.StarGiftUnique.Gift.ID != upgraded.Unique.ID { + t.Fatalf("owner upgrade service message = %+v", ownerMessage) + } + + var ( + issued, uniqueCount, commandCount int + reason string + ) + if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, poolRevision.ID).Scan(&issued); err != nil { + t.Fatal(err) + } + if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM unique_star_gifts WHERE source_saved_gift_id=$1`, savedID).Scan(&uniqueCount); err != nil { + t.Fatal(err) + } + if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM star_gift_upgrade_commands WHERE source_saved_gift_id=$1`, savedID).Scan(&commandCount); err != nil { + t.Fatal(err) + } + if err := pool.QueryRow(ctx, `SELECT reason FROM stars_transactions WHERE user_id=$1 ORDER BY id DESC LIMIT 1`, owner.ID).Scan(&reason); err != nil { + t.Fatal(err) + } + if issued != 1 || uniqueCount != 1 || commandCount != 1 || reason != string(domain.StarsReasonGiftUpgrade) { + t.Fatalf("durable aggregate issued=%d unique=%d command=%d reason=%q", issued, uniqueCount, commandCount, reason) + } + + replayed, err := upgrades.UpgradeStarGift(ctx, req) + if err != nil { + t.Fatalf("replay upgrade: %v", err) + } + if !replayed.Duplicate || replayed.Unique.ID != upgraded.Unique.ID || replayed.Balance.Balance != 900 { + t.Fatalf("replayed upgrade = %+v", replayed) + } + conflictingReplay := req + conflictingReplay.KeepOriginalDetails = false + if _, err := upgrades.UpgradeStarGift(ctx, conflictingReplay); err == nil { + t.Fatal("same command key with a changed semantic payload must not replay") + } + if _, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{ + UserID: owner.ID, Ref: req.Ref, ChargeStars: 100, FormID: 992, + CommandKey: "different-" + suffix, Date: 1700001003, + }); !errors.Is(err, domain.ErrStarGiftAlreadyUpgraded) { + t.Fatalf("second logical upgrade err = %v", err) + } + bal, err := stars.GetBalance(ctx, owner.ID) + if err != nil || bal.Balance != 900 { + t.Fatalf("balance after retries = %+v err %v", bal, err) + } + + prepaidSavedID, err := gifts.Create(ctx, domain.SavedStarGift{ + Owner: ownerPeer, FromUserID: sender.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID, + // A later pool revision may raise the current price; the historical paid + // amount remains an entitlement instead of being compared to that price. + MsgID: 700002, Date: 1700001004, ConvertStars: 25, PrepaidUpgradeStars: 50, + }) + if err != nil { + t.Fatalf("create prepaid saved gift: %v", err) + } + prepaid, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{ + UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 700002}, + RequirePrepaid: true, CommandKey: "prepaid-" + suffix, Date: 1700001005, + }) + if err != nil { + t.Fatalf("free prepaid upgrade: %v", err) + } + if prepaid.Saved.ID != prepaidSavedID || prepaid.Unique.Num != 2 || prepaid.Balance.Balance != 900 || + prepaid.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique == nil || + !prepaid.Send.RecipientMessage.Media.ServiceAction.StarGiftUnique.PrepaidUpgrade { + t.Fatalf("prepaid upgrade = %+v", prepaid) + } + + insufficientSavedID, err := gifts.Create(ctx, domain.SavedStarGift{ + Owner: ownerPeer, FromUserID: sender.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID, + MsgID: 700003, Date: 1700001006, ConvertStars: 25, + }) + if err != nil { + t.Fatalf("create insufficient saved gift: %v", err) + } + if _, err := stars.Debit(ctx, owner.ID, 850, domain.StarsReasonReaction, + domain.Peer{Type: domain.PeerTypeChannel, ID: 777001}, 1700001007, "paid reaction", ""); err != nil { + t.Fatalf("seed isolated paid reaction debit: %v", err) + } + if _, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{ + UserID: owner.ID, Ref: domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 700003}, + ChargeStars: 100, CommandKey: "insufficient-" + suffix, Date: 1700001008, + }); !errors.Is(err, domain.ErrStarsInsufficient) { + t.Fatalf("insufficient upgrade err = %v", err) + } + insufficientSaved, found, err := gifts.GetByRef(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 700003}) + if err != nil || !found || insufficientSaved.ID != insufficientSavedID || insufficientSaved.UniqueGiftID != 0 { + t.Fatalf("saved gift after rejected upgrade = %+v found %v err %v", insufficientSaved, found, err) + } + if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, poolRevision.ID).Scan(&issued); err != nil || issued != 2 { + t.Fatalf("issued after rejected upgrade = %d err %v, want 2", issued, err) + } + if err := pool.QueryRow(ctx, `SELECT reason FROM stars_transactions WHERE user_id=$1 ORDER BY id DESC LIMIT 1`, owner.ID).Scan(&reason); err != nil || reason != string(domain.StarsReasonReaction) { + t.Fatalf("paid reaction ledger reason after rejected upgrade = %q err %v", reason, err) + } + + collection, err := gifts.CreateCollection(ctx, ownerPeer, "Favorites", []int64{savedID}) + if err != nil { + t.Fatalf("create unique collection: %v", err) + } + filtered, err := gifts.ListByOwnerFiltered(ctx, domain.SavedStarGiftFilter{Owner: ownerPeer, CollectionID: collection.CollectionID, Limit: 10}) + if err != nil || filtered.Count != 1 || len(filtered.Gifts) != 1 || filtered.Gifts[0].UniqueGiftID != upgraded.Unique.ID { + t.Fatalf("collection filter = %+v err %v", filtered, err) + } + if err := gifts.SetPinned(ctx, ownerPeer, []int64{savedID}); err != nil { + t.Fatalf("pin unique gift: %v", err) + } + pinned, found, err := gifts.GetByRef(ctx, req.Ref) + if err != nil || !found || pinned.PinnedOrder != 1 || len(pinned.CollectionIDs) != 1 || pinned.CollectionIDs[0] != collection.CollectionID { + t.Fatalf("pinned saved gift = %+v found %v err %v", pinned, found, err) + } + + concurrentOwner := createTestUser(t, ctx, users, "+1778"+suffix+"43", "ConcurrentOwner", "") + concurrentPeer := domain.Peer{Type: domain.PeerTypeUser, ID: concurrentOwner.ID} + if _, err := gifts.Create(ctx, domain.SavedStarGift{ + Owner: concurrentPeer, FromUserID: sender.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID, + MsgID: 700004, Date: 1700001010, ConvertStars: 25, + }); err != nil { + t.Fatalf("create concurrent upgrade target: %v", err) + } + if _, _, err := stars.EnsureGrant(ctx, concurrentOwner.ID, 150, 1700001011); err != nil { + t.Fatalf("grant concurrent balance: %v", err) + } + type concurrentDebitResult struct { + kind string + err error + } + start := make(chan struct{}) + results := make(chan concurrentDebitResult, 2) + go func() { + <-start + _, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{ + UserID: concurrentOwner.ID, Ref: domain.SavedStarGiftRef{Owner: concurrentPeer, MsgID: 700004}, + ChargeStars: 100, FormID: 993, CommandKey: "concurrent-upgrade-" + suffix, Date: 1700001012, + }) + results <- concurrentDebitResult{kind: "gift_upgrade", err: err} + }() + go func() { + <-start + _, err := stars.Debit(ctx, concurrentOwner.ID, 100, domain.StarsReasonReaction, + domain.Peer{Type: domain.PeerTypeChannel, ID: 777002}, 1700001012, "paid reaction", "") + results <- concurrentDebitResult{kind: "paid_reaction", err: err} + }() + close(start) + firstResult, secondResult := <-results, <-results + successes := 0 + for _, result := range []concurrentDebitResult{firstResult, secondResult} { + if result.err == nil { + successes++ + continue + } + if !errors.Is(result.err, domain.ErrStarsInsufficient) { + t.Fatalf("concurrent %s err = %v, want Stars insufficient for loser", result.kind, result.err) + } + } + if successes != 1 { + t.Fatalf("concurrent debit results = %+v / %+v, want exactly one success", firstResult, secondResult) + } + concurrentBalance, err := stars.GetBalance(ctx, concurrentOwner.ID) + if err != nil || concurrentBalance.Balance != 50 { + t.Fatalf("concurrent balance = %+v err %v, want 50", concurrentBalance, err) + } + reasonRows, err := pool.Query(ctx, `SELECT reason FROM stars_transactions WHERE user_id=$1 AND amount<0 ORDER BY id`, concurrentOwner.ID) + if err != nil { + t.Fatalf("list concurrent debit reasons: %v", err) + } + var debitReasons []string + for reasonRows.Next() { + var got string + if err := reasonRows.Scan(&got); err != nil { + reasonRows.Close() + t.Fatal(err) + } + debitReasons = append(debitReasons, got) + } + if err := reasonRows.Err(); err != nil { + reasonRows.Close() + t.Fatal(err) + } + reasonRows.Close() + if len(debitReasons) != 1 || (debitReasons[0] != string(domain.StarsReasonGiftUpgrade) && debitReasons[0] != string(domain.StarsReasonReaction)) { + t.Fatalf("concurrent debit reasons = %+v, want exactly one isolated business reason", debitReasons) + } + + soldOutEntry, err := gifts.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{ + Title: "Nova", Stars: 25, ConvertStars: 10, Enabled: true, + Document: collectibleTestDocument(baseDocumentID+100, "nova.tgs"), + Blob: collectibleTestBlob(baseDocumentID+100, "nova"), Animation: collectibleTestAnimation("nova.tgs"), + Actor: "integration", CommandID: "soldout-catalog-" + suffix, + }) + if err != nil { + t.Fatalf("create sold-out catalog: %v", err) + } + soldOutRevision, err := gifts.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{ + GiftID: soldOutEntry.Gift.ID, UpgradeStars: 10, SupplyTotal: 1, SlugPrefix: "nova-" + suffix, + Models: []domain.StarGiftCollectibleAttribute{{ + Kind: domain.StarGiftCollectibleModel, Name: "Nova", RarityPermille: 1000, + Document: collectibleTestDocumentPtr(baseDocumentID+101, "nova-model.tgs"), + Blob: collectibleTestBlobPtr(baseDocumentID+101, "nova-model"), Animation: collectibleTestAnimationPtr("nova-model.tgs"), + }}, + Patterns: []domain.StarGiftCollectibleAttribute{{ + Kind: domain.StarGiftCollectiblePattern, Name: "Ray", RarityPermille: 1000, + Document: collectibleTestDocumentPtr(baseDocumentID+102, "nova-pattern.tgs"), + Blob: collectibleTestBlobPtr(baseDocumentID+102, "nova-pattern"), Animation: collectibleTestAnimationPtr("nova-pattern.tgs"), + }}, + Backdrops: []domain.StarGiftCollectibleAttribute{{ + Kind: domain.StarGiftCollectibleBackdrop, Name: "Void", BackdropID: 2, + CenterColor: 0x101010, EdgeColor: 0x202020, PatternColor: 0x303030, TextColor: 0xffffff, + RarityPermille: 1000, + }}, + Actor: "integration", CommandID: "soldout-pool-" + suffix, + }) + if err != nil { + t.Fatalf("publish sold-out pool: %v", err) + } + soldOutOwner := createTestUser(t, ctx, users, "+1778"+suffix+"44", "SoldOutOwner", "") + soldOutPeer := domain.Peer{Type: domain.PeerTypeUser, ID: soldOutOwner.ID} + for index, msgID := range []int{700010, 700011} { + if _, err := gifts.Create(ctx, domain.SavedStarGift{ + Owner: soldOutPeer, FromUserID: sender.ID, GiftID: soldOutEntry.Gift.ID, RevisionID: soldOutEntry.Gift.RevisionID, + MsgID: msgID, Date: 1700001020 + index, ConvertStars: 10, + }); err != nil { + t.Fatalf("create sold-out target %d: %v", msgID, err) + } + } + if _, _, err := stars.EnsureGrant(ctx, soldOutOwner.ID, 100, 1700001022); err != nil { + t.Fatalf("grant sold-out owner balance: %v", err) + } + if _, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{ + UserID: soldOutOwner.ID, Ref: domain.SavedStarGiftRef{Owner: soldOutPeer, MsgID: 700010}, + ChargeStars: 10, CommandKey: "soldout-first-" + suffix, Date: 1700001023, + }); err != nil { + t.Fatalf("fill collectible supply: %v", err) + } + balanceBeforeSoldOut, _ := stars.GetBalance(ctx, soldOutOwner.ID) + if _, err := upgrades.UpgradeStarGift(ctx, domain.StarGiftUpgradeRequest{ + UserID: soldOutOwner.ID, Ref: domain.SavedStarGiftRef{Owner: soldOutPeer, MsgID: 700011}, + ChargeStars: 10, CommandKey: "soldout-second-" + suffix, Date: 1700001024, + }); !errors.Is(err, domain.ErrStarGiftCollectibleSoldOut) { + t.Fatalf("sold-out upgrade err = %v", err) + } + balanceAfterSoldOut, _ := stars.GetBalance(ctx, soldOutOwner.ID) + var soldOutIssued int + if err := pool.QueryRow(ctx, `SELECT issued FROM star_gift_collectible_revisions WHERE id=$1`, soldOutRevision.ID).Scan(&soldOutIssued); err != nil || soldOutIssued != 1 || balanceAfterSoldOut.Balance != balanceBeforeSoldOut.Balance { + t.Fatalf("sold-out state issued=%d balance=%d->%d err=%v", soldOutIssued, balanceBeforeSoldOut.Balance, balanceAfterSoldOut.Balance, err) + } + + ordinaryCollection, err := gifts.CreateCollection(ctx, ownerPeer, "Ordinary", []int64{insufficientSavedID}) + if err != nil { + t.Fatalf("create ordinary collection: %v", err) + } + converted, err := gifts.MarkConverted(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 700003}) + if err != nil || !converted.Converted || converted.PinnedOrder != 0 || len(converted.CollectionIDs) != 0 { + t.Fatalf("convert collection member = %+v err %v", converted, err) + } + collections, err := gifts.ListCollections(ctx, ownerPeer) + if err != nil { + t.Fatalf("list collections after conversion: %v", err) + } + foundOrdinary := false + for _, got := range collections { + if got.CollectionID != ordinaryCollection.CollectionID { + continue + } + foundOrdinary = true + if len(got.GiftIDs) != 0 || got.Hash != domain.StarGiftCollectionHash(got.Title, nil) || got.Hash == ordinaryCollection.Hash { + t.Fatalf("ordinary collection after conversion = %+v", got) + } + } + if !foundOrdinary { + t.Fatal("ordinary collection disappeared after member conversion") + } + filteredAfterConvert, err := gifts.ListByOwnerFiltered(ctx, domain.SavedStarGiftFilter{ + Owner: ownerPeer, CollectionID: ordinaryCollection.CollectionID, Limit: 10, + }) + if err != nil || filteredAfterConvert.Count != 0 || len(filteredAfterConvert.Gifts) != 0 { + t.Fatalf("converted collection filter = %+v err %v, want empty", filteredAfterConvert, err) + } +} + +func collectibleTestAnimation(name string) domain.StarGiftAnimation { + return domain.StarGiftAnimation{ + SourceName: name, SourceFormat: domain.StarGiftAnimationTGS, + JSON: []byte(`{"v":"5.7","w":512,"h":512,"fr":30,"ip":0,"op":30,"layers":[{}]}`), + TGS: []byte("test"), SHA256: make([]byte, 32), Width: 512, Height: 512, FrameRate: 30, OutPoint: 30, + } +} + +func collectibleTestAnimationPtr(name string) *domain.StarGiftAnimation { + animation := collectibleTestAnimation(name) + return &animation +} + +func collectibleTestDocument(id int64, name string) domain.Document { + return domain.Document{ + ID: id, AccessHash: id + 100, FileReference: []byte("collectible-test"), Date: 1700001000, + MimeType: "application/x-tgsticker", Size: 4, DCID: 2, + Attributes: []domain.DocumentAttribute{ + {Kind: domain.DocAttrImageSize, W: 512, H: 512}, + {Kind: domain.DocAttrSticker, Alt: "🎁"}, + {Kind: domain.DocAttrFilename, FileName: name}, + }, + } +} + +func collectibleTestDocumentPtr(id int64, name string) *domain.Document { + document := collectibleTestDocument(id, name) + return &document +} + +func collectibleTestBlob(id int64, suffix string) domain.FileBlob { + return domain.FileBlob{ + LocationKey: fmt.Sprintf("doc:%d", id), Backend: domain.MediaBackendLocalFS, + ObjectKey: "collectible-integration-" + suffix, Size: 4, SHA256: make([]byte, 32), MimeType: "application/x-tgsticker", + } +} + +func collectibleTestBlobPtr(id int64, suffix string) *domain.FileBlob { + blob := collectibleTestBlob(id, suffix) + return &blob +} diff --git a/internal/store/postgres/star_gift_integration_test.go b/internal/store/postgres/star_gift_integration_test.go index 39c1864c..778a013b 100644 --- a/internal/store/postgres/star_gift_integration_test.go +++ b/internal/store/postgres/star_gift_integration_test.go @@ -3,13 +3,15 @@ package postgres import ( "context" "errors" + "fmt" "testing" + "time" "telesrv/internal/domain" ) -// TestStarGiftStorePostgres 回归迁移 0011:用户收到礼物实例对真实 PG 的 CRUD -// (创建 / keyset 分页 / excludeUnsaved / 隐藏切换 / 转换幂等)。 +// TestStarGiftStorePostgres 回归迁移 0089:目录不可变版本与用户收到礼物实例对真实 PG 的 CRUD +// (版本固定 / 创建 / keyset 分页 / excludeUnsaved / 隐藏切换 / 转换幂等)。 func TestStarGiftStorePostgres(t *testing.T) { pool := testPool(t) ctx := context.Background() @@ -26,21 +28,72 @@ func TestStarGiftStorePostgres(t *testing.T) { t.Fatalf("create sender: %v", err) } ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID} + docID := time.Now().UnixNano() & 0x7fffffffffffffff + documentIDs := []int64{docID} + locationKeys := []string{"doc:" + fmt.Sprint(docID)} + entry, err := st.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{ + Stars: 50, ConvertStars: 50, Enabled: true, Document: domain.Document{ + ID: docID, AccessHash: docID + 1, MimeType: "application/x-tgsticker", Size: 4, DCID: 2, + Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}, + }, + Blob: domain.FileBlob{LocationKey: "doc:" + fmt.Sprint(docID), Backend: domain.MediaBackendLocalFS, ObjectKey: "test-star-gift", Size: 4, SHA256: make([]byte, 32), MimeType: "application/x-tgsticker"}, + Animation: domain.StarGiftAnimation{JSON: []byte(`{"v":"5.7","w":512,"h":512,"fr":30,"ip":0,"op":30,"layers":[{}]}`), SHA256: make([]byte, 32), SourceFormat: domain.StarGiftAnimationTGS, Width: 512, Height: 512}, + Actor: "test", CommandID: "test-star-gift-" + suffix, + }) + if err != nil { + t.Fatalf("create catalog gift: %v", err) + } t.Cleanup(func() { _, _ = pool.Exec(ctx, "DELETE FROM peer_star_gifts WHERE owner_peer_id IN ($1, $2)", owner.ID, int64(987654321)) + tx, _ := pool.Begin(ctx) + if tx != nil { + _, _ = tx.Exec(ctx, "DELETE FROM star_gift_catalog WHERE gift_id=$1", entry.Gift.ID) + _, _ = tx.Exec(ctx, "DELETE FROM star_gift_catalog_revisions WHERE gift_id=$1", entry.Gift.ID) + _, _ = tx.Exec(ctx, "DELETE FROM file_blobs WHERE location_key = ANY($1::text[])", locationKeys) + _, _ = tx.Exec(ctx, "DELETE FROM documents WHERE id = ANY($1::bigint[])", documentIDs) + _ = tx.Commit(ctx) + } _, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, from.ID}) }) // 创建三份礼物(msg_id 递增)。 for i := 0; i < 3; i++ { if _, err := st.Create(ctx, domain.SavedStarGift{ - Owner: ownerPeer, FromUserID: from.ID, GiftID: 8001, MsgID: 100 + i, + Owner: ownerPeer, FromUserID: from.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID, MsgID: 100 + i, Date: 1700000000 + i, ConvertStars: 50, }); err != nil { t.Fatalf("create gift #%d: %v", i, err) } } + // active revision 更新后,已收到的礼物必须继续固定到购买瞬间的 immutable revision。 + docID2 := docID + 1 + documentIDs = append(documentIDs, docID2) + locationKeys = append(locationKeys, "doc:"+fmt.Sprint(docID2)) + updated, err := st.CreateCatalogRevision(ctx, domain.StarGiftCatalogWrite{ + GiftID: entry.Gift.ID, Title: "Revision 2", Stars: 75, ConvertStars: 25, Enabled: true, + Document: domain.Document{ + ID: docID2, AccessHash: docID2 + 1, MimeType: "application/x-tgsticker", Size: 4, DCID: 2, + Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}, + }, + Blob: domain.FileBlob{LocationKey: "doc:" + fmt.Sprint(docID2), Backend: domain.MediaBackendLocalFS, ObjectKey: "test-star-gift-v2", Size: 4, SHA256: make([]byte, 32), MimeType: "application/x-tgsticker"}, + Animation: domain.StarGiftAnimation{JSON: []byte(`{"v":"5.7","w":512,"h":512,"fr":30,"ip":0,"op":30,"layers":[{}]}`), SHA256: make([]byte, 32), SourceFormat: domain.StarGiftAnimationTGS, Width: 512, Height: 512}, + Actor: "test", CommandID: "test-star-gift-v2-" + suffix, + }) + if err != nil { + t.Fatalf("create catalog revision 2: %v", err) + } + if updated.Revision != 2 || updated.Gift.RevisionID == entry.Gift.RevisionID { + t.Fatalf("revision 2 = %+v, want a new immutable revision", updated) + } + if updated.ReceivedCount != 3 { + t.Fatalf("revision 2 received count = %d, want all 3 historical instances", updated.ReceivedCount) + } + historical, found, err := st.CatalogRevision(ctx, entry.Gift.RevisionID) + if err != nil || !found || historical.Stars != 50 || historical.Sticker.ID != docID { + t.Fatalf("historical revision = %+v found %v err %v", historical, found, err) + } + // keyset 分页:每页 2,末页省略游标。 page1, err := st.ListByOwner(ctx, ownerPeer, false, "", 2) if err != nil { @@ -71,7 +124,7 @@ func TestStarGiftStorePostgres(t *testing.T) { // GetByRef(user msg_id)。 g, found, err := st.GetByRef(ctx, domain.SavedStarGiftRef{Owner: ownerPeer, MsgID: 100}) - if err != nil || !found || g.GiftID != 8001 || g.ConvertStars != 50 { + if err != nil || !found || g.GiftID != entry.Gift.ID || g.RevisionID != entry.Gift.RevisionID || g.ConvertStars != 50 { t.Fatalf("get = %+v found %v err %v", g, found, err) } @@ -101,13 +154,13 @@ func TestStarGiftStorePostgres(t *testing.T) { // 频道礼物用 saved_id 定位,和用户 msg_id 身份键隔离。 channelPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: 987654321} if _, err := st.Create(ctx, domain.SavedStarGift{ - Owner: ownerPeer, FromUserID: from.ID, GiftID: 8001, MsgID: 700, + Owner: ownerPeer, FromUserID: from.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID, MsgID: 700, Date: 1700000100, ConvertStars: 50, }); err != nil { t.Fatalf("create user gift with same msg_id namespace: %v", err) } channelSavedID, err := st.Create(ctx, domain.SavedStarGift{ - Owner: channelPeer, FromUserID: from.ID, GiftID: 8001, MsgID: 0, SavedID: 0, + Owner: channelPeer, FromUserID: from.ID, GiftID: entry.Gift.ID, RevisionID: entry.Gift.RevisionID, MsgID: 0, SavedID: 0, Date: 1700000101, ConvertStars: 50, }) if err != nil { diff --git a/internal/store/postgres/star_gift_upgrade.go b/internal/store/postgres/star_gift_upgrade.go new file mode 100644 index 00000000..3bf3dd14 --- /dev/null +++ b/internal/store/postgres/star_gift_upgrade.go @@ -0,0 +1,365 @@ +package postgres + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "errors" + "fmt" + "math/big" + "strings" + + "github.com/jackc/pgx/v5" + + "telesrv/internal/domain" + "telesrv/internal/store" + "telesrv/internal/store/postgres/sqlcgen" +) + +// StarGiftUpgradeStore is the PostgreSQL aggregate coordinator for collectible +// upgrades. It intentionally shares MessageStore's allocator and transaction +// machinery so Stars, issuance, the saved gift and durable updates commit once. +type StarGiftUpgradeStore struct { + db sqlcgen.DBTX + messages *MessageStore +} + +func NewStarGiftUpgradeStore(db sqlcgen.DBTX, messages *MessageStore) *StarGiftUpgradeStore { + return &StarGiftUpgradeStore{db: db, messages: messages} +} + +func (s *StarGiftUpgradeStore) UpgradeStarGift(ctx context.Context, req domain.StarGiftUpgradeRequest) (domain.StarGiftUpgradeResult, error) { + if s == nil || s.db == nil || s.messages == nil || req.UserID <= 0 || !req.Ref.Valid() || + req.Ref.Owner != (domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID}) || + req.ChargeStars < 0 || req.Date <= 0 || strings.TrimSpace(req.CommandKey) == "" || len(req.CommandKey) > 256 { + return domain.StarGiftUpgradeResult{}, domain.ErrStarGiftCollectibleInvalid + } + saved, found, err := NewStarGiftStore(s.db).GetByRef(ctx, req.Ref) + if err != nil { + return domain.StarGiftUpgradeResult{}, err + } + if !found || saved.FromUserID <= 0 { + return domain.StarGiftUpgradeResult{}, domain.ErrStarGiftNotFound + } + + commandKey := strings.TrimSpace(req.CommandKey) + fingerprint := sha256.Sum256([]byte(fmt.Sprintf( + "telesrv:star-gift-upgrade:v1:%s:%d:%d:%t:%d:%t", + commandKey, saved.ID, req.ChargeStars, req.RequirePrepaid, req.FormID, req.KeepOriginalDetails, + ))) + randomID := starGiftUpgradeRandomID(saved.FromUserID, req.UserID, commandKey) + placeholder := &domain.MessageMedia{ + Kind: domain.MessageMediaKindService, + ServiceAction: &domain.MessageServiceAction{ + Kind: domain.MessageServiceActionStarGiftUnique, + StarGiftUnique: &domain.MessageStarGiftUniqueAction{Upgrade: true, Saved: true}, + }, + } + messageReq := domain.SendPrivateTextRequest{ + SenderUserID: saved.FromUserID, + RecipientUserID: req.UserID, + RandomID: randomID, + Media: placeholder, + Date: req.Date, + OriginAuthKeyID: req.OriginAuthKeyID, + OriginSessionID: req.OriginSessionID, + OriginUserID: req.UserID, + IdempotencyFingerprint: fingerprint[:], + } + + var result domain.StarGiftUpgradeResult + hooks := privateSendTxHooks{ + before: func(ctx context.Context, tx pgx.Tx, messageReq *domain.SendPrivateTextRequest) error { + locked, err := lockSavedStarGiftForUpgrade(ctx, tx, req.Ref) + if err != nil { + return err + } + if locked.ID != saved.ID || locked.FromUserID != saved.FromUserID { + return domain.ErrStarGiftCollectibleInvalid + } + if locked.Converted { + return domain.ErrStarGiftAlreadyConverted + } + if locked.UniqueGiftID != 0 { + return domain.ErrStarGiftAlreadyUpgraded + } + + revision, err := lockActiveCollectibleRevision(ctx, tx, locked.GiftID) + if err != nil { + return err + } + if revision.Issued >= revision.SupplyTotal { + return domain.ErrStarGiftCollectibleSoldOut + } + if req.RequirePrepaid { + // Prepayment is an entitlement captured at gift purchase time. A + // later published revision may change the current price, but must not + // retroactively invalidate that already-paid entitlement. + if req.ChargeStars != 0 || locked.PrepaidUpgradeStars <= 0 { + return domain.ErrStarGiftCollectibleUnavailable + } + } else if req.ChargeStars != revision.UpgradeStars { + return domain.ErrStarGiftCollectibleUnavailable + } + + balance, err := debitStarGiftUpgrade(ctx, tx, req.UserID, req.ChargeStars, locked.Owner, req.Date) + if err != nil { + return err + } + modelID, err := chooseCollectibleAttribute(ctx, tx, "star_gift_collectible_models", revision.ID) + if err != nil { + return err + } + patternID, err := chooseCollectibleAttribute(ctx, tx, "star_gift_collectible_patterns", revision.ID) + if err != nil { + return err + } + backdropID, err := chooseCollectibleAttribute(ctx, tx, "star_gift_collectible_backdrops", revision.ID) + if err != nil { + return err + } + + num := revision.Issued + 1 + var uniqueID int64 + if err := tx.QueryRow(ctx, `SELECT nextval('unique_star_gift_id_seq')`).Scan(&uniqueID); err != nil { + return fmt.Errorf("allocate unique star gift id: %w", err) + } + var title string + if err := tx.QueryRow(ctx, `SELECT title FROM star_gift_catalog_revisions WHERE id=$1`, locked.RevisionID).Scan(&title); err != nil { + return fmt.Errorf("load upgrade gift title: %w", err) + } + slug := fmt.Sprintf("%s-%d", revision.SlugPrefix, num) + if _, err := tx.Exec(ctx, ` +INSERT INTO unique_star_gifts + (id, gift_id, collectible_revision_id, source_saved_gift_id, title, slug, num, + owner_peer_type, owner_peer_id, model_attribute_id, pattern_attribute_id, + backdrop_attribute_id, keep_original_details) +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`, + uniqueID, locked.GiftID, revision.ID, locked.ID, title, slug, num, + string(locked.Owner.Type), locked.Owner.ID, modelID, patternID, backdropID, req.KeepOriginalDetails); err != nil { + return fmt.Errorf("insert unique star gift: %w", err) + } + if _, err := tx.Exec(ctx, `UPDATE star_gift_collectible_revisions SET issued=issued+1 WHERE id=$1`, revision.ID); err != nil { + return fmt.Errorf("increment collectible issuance: %w", err) + } + if _, err := tx.Exec(ctx, ` +UPDATE peer_star_gifts +SET unique_gift_id=$2, prepaid_upgrade_stars=0, convert_stars=0 +WHERE id=$1 AND unique_gift_id IS NULL AND NOT converted`, locked.ID, uniqueID); err != nil { + return fmt.Errorf("upgrade saved star gift: %w", err) + } + if _, err := tx.Exec(ctx, ` +INSERT INTO star_gift_upgrade_commands + (user_id, command_key, source_saved_gift_id, form_id, unique_gift_id, balance_after) +VALUES ($1,$2,$3,$4,$5,$6)`, req.UserID, commandKey, locked.ID, req.FormID, uniqueID, balance.Balance); err != nil { + return fmt.Errorf("insert star gift upgrade command: %w", err) + } + + unique, found, err := NewStarGiftStore(tx).UniqueByID(ctx, uniqueID) + if err != nil { + return err + } + if !found { + return fmt.Errorf("new unique star gift %d disappeared", uniqueID) + } + locked.UniqueGiftID = uniqueID + locked.PrepaidUpgradeStars = 0 + locked.ConvertStars = 0 + locked.Unique = &unique + result.Saved, result.Unique, result.Balance = locked, unique, balance + messageReq.Media = &domain.MessageMedia{ + Kind: domain.MessageMediaKindService, + ServiceAction: &domain.MessageServiceAction{ + Kind: domain.MessageServiceActionStarGiftUnique, + StarGiftUnique: &domain.MessageStarGiftUniqueAction{ + Gift: unique, FromUserID: func() int64 { + if locked.NameHidden { + return 0 + } + return locked.FromUserID + }(), Peer: locked.Owner, Upgrade: true, Saved: !locked.Unsaved, + PrepaidUpgrade: req.RequirePrepaid, + }, + }, + } + return nil + }, + after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error { + ownerMessageID := sent.RecipientMessage.ID + if saved.FromUserID == req.UserID { + ownerMessageID = sent.SenderMessage.ID + } + if ownerMessageID <= 0 { + return fmt.Errorf("upgrade service message missing owner box") + } + tag, err := tx.Exec(ctx, `UPDATE peer_star_gifts SET upgrade_msg_id=$2 WHERE id=$1 AND unique_gift_id=$3`, result.Saved.ID, ownerMessageID, result.Unique.ID) + if err != nil { + return fmt.Errorf("save star gift upgrade message id: %w", err) + } + if tag.RowsAffected() != 1 { + return fmt.Errorf("save star gift upgrade message id lost aggregate row") + } + result.Saved.UpgradeMsgID = ownerMessageID + return nil + }, + } + sent, err := s.messages.sendPrivateTextWithHooks(ctx, messageReq, hooks) + if err != nil { + return domain.StarGiftUpgradeResult{}, err + } + result.Send = sent + result.Duplicate = sent.Duplicate + if sent.Duplicate { + return s.loadUpgradeReplay(ctx, req, saved, sent) + } + return result, nil +} + +func lockSavedStarGiftForUpgrade(ctx context.Context, tx pgx.Tx, ref domain.SavedStarGiftRef) (domain.SavedStarGift, error) { + where, args := savedStarGiftRefWhere(ref) + row := tx.QueryRow(ctx, ` +SELECT p.id, p.owner_peer_type, p.owner_peer_id, p.from_user_id, p.gift_id, p.catalog_revision_id, + p.msg_id, p.saved_id, p.gift_date, p.name_hidden, p.unsaved, p.converted, p.convert_stars, p.prepaid_upgrade_stars, + p.message, COALESCE(p.unique_gift_id, 0), p.upgrade_msg_id, p.pinned_order, + COALESCE((SELECT array_agg(i.collection_id ORDER BY c.sort_order, i.collection_id) + FROM star_gift_collection_items i + JOIN star_gift_collections c ON c.collection_id=i.collection_id + WHERE i.saved_gift_id=p.id), ARRAY[]::integer[]) +FROM peer_star_gifts p WHERE `+where+` FOR UPDATE`, args...) + saved, err := scanSavedStarGift(row) + if errors.Is(err, pgx.ErrNoRows) { + return domain.SavedStarGift{}, domain.ErrStarGiftNotFound + } + return saved, err +} + +func lockActiveCollectibleRevision(ctx context.Context, tx pgx.Tx, giftID int64) (domain.StarGiftCollectibleRevision, error) { + var revision domain.StarGiftCollectibleRevision + var status string + err := tx.QueryRow(ctx, ` +SELECT r.id, r.gift_id, r.upgrade_stars, r.supply_total, r.issued, r.slug_prefix, r.status +FROM star_gift_catalog c +JOIN star_gift_collectible_revisions r ON r.id=c.collectible_revision_id +WHERE c.gift_id=$1 FOR UPDATE OF r`, giftID).Scan( + &revision.ID, &revision.GiftID, &revision.UpgradeStars, &revision.SupplyTotal, + &revision.Issued, &revision.SlugPrefix, &status) + if errors.Is(err, pgx.ErrNoRows) { + return domain.StarGiftCollectibleRevision{}, domain.ErrStarGiftCollectibleUnavailable + } + if err != nil { + return domain.StarGiftCollectibleRevision{}, fmt.Errorf("lock active collectible revision: %w", err) + } + if status != "published" { + return domain.StarGiftCollectibleRevision{}, domain.ErrStarGiftCollectibleUnavailable + } + return revision, nil +} + +func debitStarGiftUpgrade(ctx context.Context, tx pgx.Tx, userID, amount int64, peer domain.Peer, date int) (domain.StarsBalance, error) { + result := domain.StarsBalance{UserID: userID} + var balance int64 + err := tx.QueryRow(ctx, `SELECT balance, granted FROM stars_balances WHERE user_id=$1 FOR UPDATE`, userID).Scan(&balance, &result.Granted) + if amount == 0 && errors.Is(err, pgx.ErrNoRows) { + return result, nil + } + if errors.Is(err, pgx.ErrNoRows) || (err == nil && balance < amount) { + return domain.StarsBalance{}, domain.ErrStarsInsufficient + } + if err != nil { + return domain.StarsBalance{}, fmt.Errorf("lock stars balance for gift upgrade: %w", err) + } + if amount == 0 { + result.Balance = balance + return result, nil + } + if err := tx.QueryRow(ctx, `UPDATE stars_balances SET balance=balance-$2, updated_at=now() WHERE user_id=$1 RETURNING balance`, userID, amount).Scan(&result.Balance); err != nil { + return domain.StarsBalance{}, fmt.Errorf("debit star gift upgrade: %w", err) + } + if err := insertStarsTxn(ctx, tx, userID, -amount, domain.StarsReasonGiftUpgrade, peer, date, "Star gift upgrade", ""); err != nil { + return domain.StarsBalance{}, err + } + return result, nil +} + +func chooseCollectibleAttribute(ctx context.Context, tx pgx.Tx, table string, revisionID int64) (int64, error) { + rows, err := tx.Query(ctx, fmt.Sprintf(`SELECT id, rarity_permille FROM %s WHERE collectible_revision_id=$1 ORDER BY sort_order, id`, table), revisionID) + if err != nil { + return 0, fmt.Errorf("list collectible attributes for issuance: %w", err) + } + defer rows.Close() + type weightedID struct { + id int64 + weight int + } + items := make([]weightedID, 0) + total := 0 + for rows.Next() { + var item weightedID + if err := rows.Scan(&item.id, &item.weight); err != nil { + return 0, err + } + items = append(items, item) + total += item.weight + } + if err := rows.Err(); err != nil { + return 0, err + } + if len(items) == 0 || total != 1000 { + return 0, domain.ErrStarGiftCollectibleInvalid + } + draw, err := rand.Int(rand.Reader, big.NewInt(int64(total))) + if err != nil { + return 0, fmt.Errorf("draw collectible attribute: %w", err) + } + value := int(draw.Int64()) + for _, item := range items { + if value < item.weight { + return item.id, nil + } + value -= item.weight + } + return 0, domain.ErrStarGiftCollectibleInvalid +} + +func starGiftUpgradeRandomID(senderID, ownerID int64, commandKey string) int64 { + sum := sha256.Sum256([]byte(fmt.Sprintf("%d:%d:%s", senderID, ownerID, commandKey))) + id := int64(binary.LittleEndian.Uint64(sum[:8]) & 0x7fffffffffffffff) + if id == 0 { + id = 1 + } + return id +} + +func (s *StarGiftUpgradeStore) loadUpgradeReplay(ctx context.Context, req domain.StarGiftUpgradeRequest, original domain.SavedStarGift, sent domain.SendPrivateTextResult) (domain.StarGiftUpgradeResult, error) { + saved, found, err := NewStarGiftStore(s.db).GetByRef(ctx, req.Ref) + if err != nil || !found || saved.UniqueGiftID == 0 { + if err == nil { + err = domain.ErrStarGiftCollectibleInvalid + } + return domain.StarGiftUpgradeResult{}, err + } + unique, found, err := NewStarGiftStore(s.db).UniqueByID(ctx, saved.UniqueGiftID) + if err != nil || !found { + if err == nil { + err = domain.ErrStarGiftCollectibleInvalid + } + return domain.StarGiftUpgradeResult{}, err + } + var commandUniqueID int64 + var balanceAfter int64 + if err := s.db.QueryRow(ctx, `SELECT unique_gift_id, balance_after FROM star_gift_upgrade_commands WHERE user_id=$1 AND command_key=$2`, req.UserID, strings.TrimSpace(req.CommandKey)).Scan(&commandUniqueID, &balanceAfter); err != nil { + return domain.StarGiftUpgradeResult{}, fmt.Errorf("load star gift upgrade replay: %w", err) + } + if commandUniqueID != unique.ID || saved.ID != original.ID { + return domain.StarGiftUpgradeResult{}, domain.ErrStarGiftCollectibleInvalid + } + uniqueCopy := unique + saved.Unique = &uniqueCopy + return domain.StarGiftUpgradeResult{ + Saved: saved, Unique: unique, Balance: domain.StarsBalance{UserID: req.UserID, Balance: balanceAfter}, + Send: sent, Duplicate: true, + }, nil +} + +var _ store.StarGiftUpgradeStore = (*StarGiftUpgradeStore)(nil) diff --git a/internal/store/star_gift.go b/internal/store/star_gift.go index 37372003..0fff4bfc 100644 --- a/internal/store/star_gift.go +++ b/internal/store/star_gift.go @@ -6,15 +6,39 @@ import ( "telesrv/internal/domain" ) -// StarGiftStore 持久化 peer 收到的 Star 礼物实例(peer_star_gifts)。礼物目录是合成的 -// 内存集合,不在此存储。 +// StarGiftStore 持久化礼物目录、不可变版本和 peer 收到的礼物实例。 type StarGiftStore interface { + // Catalog 返回启用的当前目录快照,按 sort_order/gift_id 排序。 + Catalog(ctx context.Context) ([]domain.StarGift, error) + // CatalogGift 只返回当前启用版本,供新购买校验。 + CatalogGift(ctx context.Context, giftID int64) (domain.StarGift, bool, error) + // CatalogRevision 返回不可变历史版本,供已领取礼物投影。 + CatalogRevision(ctx context.Context, revisionID int64) (domain.StarGift, bool, error) + // CreateCatalogRevision 创建新礼物或为既有礼物创建新版本,并原子切换当前版本。 + CreateCatalogRevision(ctx context.Context, write domain.StarGiftCatalogWrite) (domain.StarGiftCatalogEntry, error) + SetCatalogEnabled(ctx context.Context, giftID int64, enabled bool) (bool, error) + SetCatalogSortOrder(ctx context.Context, giftID int64, sortOrder int) (bool, error) + // AnimationJSON 返回当前版本的规范化 Lottie JSON,供管理后台安全预览。 + AnimationJSON(ctx context.Context, giftID int64) ([]byte, bool, error) + // PublishCollectibleRevision validates and atomically publishes a new immutable attribute pool. + PublishCollectibleRevision(ctx context.Context, write domain.StarGiftCollectibleWrite) (domain.StarGiftCollectibleRevision, error) + ActiveCollectibleRevision(ctx context.Context, giftID int64) (domain.StarGiftCollectibleRevision, bool, error) + CollectibleAvailability(ctx context.Context, giftIDs []int64) (map[int64]domain.StarGiftCollectibleAvailability, error) + CollectibleAnimationJSON(ctx context.Context, giftID int64, kind domain.StarGiftCollectibleAttributeKind, attributeID int64) ([]byte, bool, error) + UniqueBySlug(ctx context.Context, slug string) (domain.UniqueStarGift, bool, error) + UniqueByID(ctx context.Context, uniqueGiftID int64) (domain.UniqueStarGift, bool, error) + UniqueByIDs(ctx context.Context, uniqueGiftIDs []int64) (map[int64]domain.UniqueStarGift, error) + // Create 写一条收到的礼物实例,返回行 id;频道礼物未显式给 saved_id 时以该行 id 作为 saved_id。 Create(ctx context.Context, gift domain.SavedStarGift) (int64, error) // ListByOwner 按 id DESC keyset 分页返回某 owner 未转换的礼物;excludeUnsaved 时只返展示在资料的。 ListByOwner(ctx context.Context, owner domain.Peer, excludeUnsaved bool, offset string, limit int) (domain.SavedStarGiftPage, error) + ListByOwnerFiltered(ctx context.Context, filter domain.SavedStarGiftFilter) (domain.SavedStarGiftPage, error) // GetByRef 按协议引用取礼物实例:用户用 msg_id,频道用 saved_id。 GetByRef(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, bool, error) + // ResolveSavedIDs resolves an ordered batch of protocol references without + // per-gift round trips. Every ref must belong to owner and resolve to a live gift. + ResolveSavedIDs(ctx context.Context, owner domain.Peer, refs []domain.SavedStarGiftRef) ([]int64, error) // CountByOwner 返回某 owner 展示在资料的礼物数(非转换、非隐藏),供 full.stargifts_count。 CountByOwner(ctx context.Context, owner domain.Peer) (int, error) // SetUnsaved 切换礼物在资料的展示(saveStarGift);返回是否命中一行。 @@ -22,4 +46,18 @@ type StarGiftStore interface { // MarkConverted 幂等地把礼物标记为已转换(convertStarGift),返回该行供调用方据 ConvertStars // 入账;已转换返回 domain.ErrStarGiftAlreadyConverted,不存在返回 domain.ErrStarGiftNotFound。 MarkConverted(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, error) + + ListCollections(ctx context.Context, owner domain.Peer) ([]domain.StarGiftCollection, error) + CreateCollection(ctx context.Context, owner domain.Peer, title string, savedGiftIDs []int64) (domain.StarGiftCollection, error) + UpdateCollection(ctx context.Context, owner domain.Peer, collectionID int, patch domain.StarGiftCollectionPatch) (domain.StarGiftCollection, error) + DeleteCollection(ctx context.Context, owner domain.Peer, collectionID int) (bool, error) + ReorderCollections(ctx context.Context, owner domain.Peer, collectionIDs []int) error + SetPinned(ctx context.Context, owner domain.Peer, savedGiftIDs []int64) error +} + +// StarGiftUpgradeStore owns the aggregate transaction spanning Stars, the +// collectible issuance state, the saved gift terminal state and durable +// private service-message updates. +type StarGiftUpgradeStore interface { + UpgradeStarGift(ctx context.Context, req domain.StarGiftUpgradeRequest) (domain.StarGiftUpgradeResult, error) }